├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── dub.json ├── dub.selections.json ├── exeexcp ├── graphql.yaml ├── perftest ├── calc.d ├── dub.json ├── dub.selections.json ├── run.sh ├── schema.docs.graphql ├── schema.gql └── source │ ├── app.d │ └── countvisitor.d ├── source └── graphql │ ├── argumentextractor.d │ ├── argumentextractortests.d │ ├── ast.d │ ├── astselector.d │ ├── builder.d │ ├── client │ ├── codegen.d │ ├── document.d │ ├── query.d │ └── vibe.d │ ├── constants.d │ ├── directives.d │ ├── exception.d │ ├── graphql.d │ ├── helper.d │ ├── lexer.d │ ├── package.d │ ├── parser.d │ ├── parsertests.d │ ├── schema │ ├── directives.d │ ├── helper.d │ ├── introspectiontypes.d │ ├── package.d │ ├── resolver.d │ ├── toschemafile.d │ ├── typeconversions.d │ └── types.d │ ├── starwars │ ├── data.d │ ├── introspection.d │ ├── query.d │ ├── schema.d │ ├── types.d │ └── validation.d │ ├── testschema.d │ ├── tokenmodule.d │ ├── traits.d │ ├── treevisitor.d │ ├── uda.d │ ├── validation │ ├── exception.d │ ├── querybased.d │ └── schemabased.d │ └── visitor.d ├── starwarsschemaparsetest.graphql └── test ├── README.md ├── dub.json ├── dub.selections.json ├── introspectionquery.gql ├── schema.gql ├── schema2.gql └── source ├── app.d ├── testdata.d ├── testdata2.d └── testqueries.d /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | tab_width = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 80 13 | 14 | [*.yaml] 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | Test: 13 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - windows-latest 21 | - ubuntu-latest 22 | - macOS-latest 23 | compiler: 24 | - 'dmd-latest' 25 | - 'ldc-latest' 26 | - 'dmd-beta' 27 | # - 'ldc-beta' # the tests crash 28 | steps: 29 | - uses: actions/checkout@v2 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Install compiler 34 | uses: dlang-community/setup-dlang@v1 35 | with: 36 | compiler: ${{ matrix.compiler }} 37 | 38 | - name: Style Lint 39 | continue-on-error: true # The make file fails 40 | run: | 41 | make style_lint 42 | 43 | - name: Test 44 | run: | 45 | dub test 46 | 47 | - name: Test 48 | working-directory: ./test 49 | run: | 50 | dub build 51 | ./test --onlyRunTests 52 | 53 | - name: Test individual sub-packages 54 | run: | 55 | dub describe | 56 | jq -r '.packages[] | .name' | 57 | sed -n 's/^graphqld//p' | 58 | while read -r p ; do 59 | dub test $p 60 | done 61 | 62 | Skip: 63 | if: "contains(github.event.head_commit.message, '[skip ci]')" 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Skip CI 🚫 67 | run: echo skip CI 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | *.o 5 | *.obj 6 | *.a 7 | *.lib 8 | *-test-library 9 | *.exe 10 | __test__*__ 11 | *.lst 12 | *.swp 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | handWrittenFiles=source/graphql/argumentextractor.d \ 2 | source/graphql/constants.d \ 3 | source/graphql/helper.d \ 4 | source/graphql/testschema.d \ 5 | source/graphql/parsertests.d \ 6 | source/graphql/tokenmodule.d \ 7 | source/graphql/uda.d \ 8 | source/graphql/builder.d \ 9 | source/graphql/graphql.d \ 10 | source/graphql/traits.d \ 11 | source/graphql/schema/directives.d \ 12 | source/graphql/schema/helper.d \ 13 | source/graphql/schema/introspectiontypes.d \ 14 | source/graphql/schema/package.d \ 15 | source/graphql/schema/resolver.d \ 16 | source/graphql/schema/typeconversions.d \ 17 | source/graphql/schema/types.d \ 18 | source/graphql/validation/exception.d \ 19 | source/graphql/validation/querybased.d \ 20 | source/graphql/validation/schemabased.d \ 21 | source/graphql/astselector.d \ 22 | source/graphql/directives.d 23 | 24 | gen: 25 | ../Darser/darser -i graphql.yaml \ 26 | -a source/graphql/ast.d -b "graphql" \ 27 | -p source/graphql/parser.d -q "graphql" \ 28 | -e source/graphql/exception.d -g "graphql" \ 29 | -v source/graphql/visitor.d -w "graphql" \ 30 | -t source/graphql/treevisitor.d -r "graphql" \ 31 | -u "graphql" -s "graphql" \ 32 | --safe 33 | 34 | cat exeexcp >> source/graphql/exception.d 35 | 36 | style_lint: 37 | #@echo "Enforce braces on the same line" 38 | #grep -nrE '^[\t ]*{' $$(find source -name '*.d'); test $$? -eq 1 39 | 40 | @echo "Check for whitespace indentation" 41 | grep -nr '^[ ]' source ; test $$? -eq 1 42 | 43 | @echo "Enforce whitespace before opening parenthesis" 44 | grep -nrE "\<(for|foreach|foreach_reverse|if|while|switch|catch|version) \(" $$(find source -name '*.d') ; test $$? -eq 1 45 | 46 | @echo "Enforce no whitespace after opening parenthesis" 47 | grep -nrE "\<(version) \( " $$(find source -name '*.d') ; test $$? -eq 1 48 | 49 | @echo "Enforce whitespace between colon(:) for import statements (doesn't catch everything)" 50 | grep -nr 'import [^/,=]*:.*;' $$(find source -name '*.d') | grep -vE "import ([^ ]+) :\s"; test $$? -eq 1 51 | 52 | @echo "Check for package wide std.algorithm imports" 53 | grep -nr 'import std.algorithm : ' $$(find source -name '*.d') ; test $$? -eq 1 54 | 55 | @echo "Enforce no space between assert and the opening brace, i.e. assert(" 56 | grep -nrE 'assert \(' $$(find source -name '*.d') ; test $$? -eq 1 57 | 58 | @echo "Enforce space between a .. b" 59 | grep -nrE '[[:alnum:]][.][.][[:alnum:]]|[[:alnum:]] [.][.][[:alnum:]]|[[:alnum:]][.][.] [[:alnum:]]' $$(find source -name '*.d' | grep -vE 'std/string.d|std/uni.d') ; test $$? -eq 1 60 | 61 | @echo "Enforce space between binary operators" 62 | grep -nrE "[[:alnum:]](==|!=|<=|<<|>>|>>>|^^)[[:alnum:]]|[[:alnum:]] (==|!=|<=|<<|>>|>>>|^^)[[:alnum:]]|[[:alnum:]](==|!=|<=|<<|>>|>>>|^^) [[:alnum:]]" $$(find source -name '*.d'); test $$? -eq 1 63 | 64 | @echo "Check line length" 65 | grep -nr '.\{81\}' $(handWrittenFiles) ; test $$? -eq 1 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphqlD: A graphql implementation for the D Programming language 2 | 3 | ![CI](https://github.com/burner/graphqld/workflows/ci/badge.svg) 4 | 5 | Graphql is a query language for apis. 6 | Given a query like for the schema in folder test. 7 | 8 | ``` 9 | { 10 | shipsselection(ids:[44,45]) { 11 | id 12 | commander { 13 | name 14 | } 15 | } 16 | } 17 | ``` 18 | 19 | You get back json that looks like: 20 | ```JS 21 | { 22 | "error": [], 23 | "data": { 24 | "shipsselection": [ 25 | { 26 | "id": 44, 27 | "commander": { 28 | "name": "Kathryn Janeway" 29 | } 30 | }, 31 | { 32 | "id": 45, 33 | "commander": { 34 | "name": "Jonathan Archer" 35 | } 36 | } 37 | ] 38 | } 39 | } 40 | ``` 41 | 42 | Graphiql type-ahead works, this makes schema introspection a lot nicer. 43 | 44 | ## Features 45 | This graphql implementation is based on June 2018 spec. 46 | 47 | ### Operation Execution 48 | - [x] Scalars 49 | - [x] Objects 50 | - [x] Lists of objects/interfaces 51 | - [x] Interfaces 52 | - [x] Unions 53 | - [x] Arguments 54 | - [x] Variables 55 | - [x] Fragments 56 | - [x] Directives 57 | - [x] Include 58 | - [x] Skip 59 | - [?] Custom (Requires changing the graphqld source) 60 | - [x] Enumerations 61 | - [x] Input Objects 62 | - [x] Mutations 63 | - [ ] Subscriptions (This needs vibe.d websocket integration) 64 | - [x] Async execution (when used with vibe.d blocking resolver are async by 65 | default) 66 | 67 | ### Validation 68 | - [ ] Arguments of correct type 69 | - [ ] Default values of correct type 70 | - [x] Fields on correct type 71 | - [x] Fragments on composite types 72 | - [X] Known argument names 73 | - [x] Executable Definition 74 | - [ ] Known directives 75 | - [x] Known fragment names 76 | - [x] Known type names 77 | - [x] Lone anonymous operations 78 | - [x] No fragment cycles 79 | - [x] No undefined variables 80 | - [x] No unused fragments 81 | - [x] No unused variables 82 | - [x] Overlapping fields can be merged (this is done during execution) 83 | - [x] Possible fragment spreads 84 | - [ ] Provide non-null arguments 85 | - [x] Scalar leafs 86 | - [x] Unique argument names 87 | - [x] Unique directives per location 88 | - [x] Unique fragment names 89 | - [x] Unique input field names 90 | - [x] Unique operation names 91 | - [x] Unique variable names 92 | - [ ] Variables are input types (this is actually a strange requirement) 93 | - [x] Variables in allowed position 94 | - [x] Single root field 95 | 96 | ### Schema Introspection 97 | - [x] __typename 98 | - [x] __type 99 | - [x] name 100 | - [x] kind 101 | - [x] description 102 | - [x] fields 103 | - [x] interfaces 104 | - [x] possibleTypes 105 | - [x] enumValues 106 | - [x] inputFields 107 | - [x] ofType 108 | - [x] __schema 109 | - [x] types 110 | - [x] queryType 111 | - [x] mutationType 112 | - [x] subscriptionType 113 | - [x] directives 114 | 115 | ### Comfort Features 116 | - [ ] Query AST cache 117 | - [ ] Json to resolver argument extractor 118 | - [ ] SQL query generation from AST 119 | - [x] Custom Leaf types (e.g. GQLDCustomLeaf!(std.datetime.DateTime)) 120 | 121 | Thank you to [graphql-dot](https://github.com/graphql-dotnet/graphql-dotnet) 122 | for the excelent list of features 123 | 124 | ## Documentation 125 | The Documentation is still WIP, please have a look at the vibe.d project in the 126 | test folder. 127 | This [file](test/source/app.d:430) gives a good overview on how to use graphqld 128 | with vibe.d. 129 | 130 | ## Contributing 131 | PRs are always welcome! 132 | 133 | # About Kaleidic Associates 134 | We are a boutique consultancy that advises a small number of hedge fund clients. We are 135 | not accepting new clients currently, but if you are interested in working either remotely 136 | or locally in London or Hong Kong, and if you are a talented hacker with a moral compass 137 | who aspires to excellence then feel free to drop me a line: laeeth at kaleidic.io 138 | 139 | We work with our partner Symmetry Investments, and some background on the firm can be 140 | found here: 141 | 142 | http://symmetryinvestments.com/about-us/ 143 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Robert burner Schadek" 4 | ], 5 | "copyright": "Copyright © 2019, Robert burner Schadek", 6 | "dependencies": { 7 | "graphqld:client": "*", 8 | "graphqld:server": "*" 9 | }, 10 | "description": "A library to handle the GraphQL Protocol", 11 | "dflags": [ 12 | "-d" 13 | ], 14 | "license": "LGPL3", 15 | "name": "graphqld", 16 | "sourceFiles": [ 17 | "source/graphql/starwars/data.d", 18 | "source/graphql/starwars/schema.d", 19 | "source/graphql/starwars/types.d", 20 | "source/graphql/starwars/introspection.d", 21 | "source/graphql/starwars/query.d", 22 | "source/graphql/starwars/validation.d", 23 | "source/graphql/package.d" 24 | ], 25 | "sourcePaths": [], 26 | "subPackages": [ 27 | { 28 | "name": "lexer", 29 | "sourceFiles": [ 30 | "source/graphql/constants.d", 31 | "source/graphql/tokenmodule.d", 32 | "source/graphql/lexer.d" 33 | ], 34 | "sourcePaths": [] 35 | }, 36 | { 37 | "name": "exception", 38 | "sourceFiles": [ 39 | "source/graphql/exception.d" 40 | ], 41 | "sourcePaths": [] 42 | }, 43 | { 44 | "dependencies": { 45 | "graphqld:exception": "*", 46 | "graphqld:lexer": "*" 47 | }, 48 | "name": "parser", 49 | "sourceFiles": [ 50 | "source/graphql/ast.d", 51 | "source/graphql/parser.d", 52 | "source/graphql/parsertests.d", 53 | "source/graphql/visitor.d", 54 | "source/graphql/treevisitor.d" 55 | ], 56 | "sourcePaths": [] 57 | }, 58 | { 59 | "dependencies": { 60 | "vibe-d:data": ">=0.9.0" 61 | }, 62 | "name": "uda", 63 | "sourceFiles": [ 64 | "source/graphql/uda.d" 65 | ], 66 | "sourcePaths": [] 67 | }, 68 | { 69 | "dependencies": { 70 | "fixedsizearray": ">=1.3.0", 71 | "graphqld:exception": "*", 72 | "graphqld:parser": "*", 73 | "graphqld:uda": "*", 74 | "nullablestore": ">=2.1.0", 75 | "vibe-d": ">=0.9.0" 76 | }, 77 | "name": "server", 78 | "sourceFiles": [ 79 | "source/graphql/argumentextractor.d", 80 | "source/graphql/argumentextractortests.d", 81 | "source/graphql/astselector.d", 82 | "source/graphql/builder.d", 83 | "source/graphql/directives.d", 84 | "source/graphql/helper.d", 85 | "source/graphql/schema/directives.d", 86 | "source/graphql/schema/helper.d", 87 | "source/graphql/schema/introspectiontypes.d", 88 | "source/graphql/schema/package.d", 89 | "source/graphql/schema/resolver.d", 90 | "source/graphql/schema/toschemafile.d", 91 | "source/graphql/schema/typeconversions.d", 92 | "source/graphql/schema/types.d", 93 | "source/graphql/testschema.d", 94 | "source/graphql/traits.d", 95 | "source/graphql/validation/exception.d", 96 | "source/graphql/validation/querybased.d", 97 | "source/graphql/validation/schemabased.d", 98 | "source/graphql/graphql.d" 99 | ], 100 | "sourcePaths": [] 101 | }, 102 | { 103 | "dependencies": { 104 | "graphqld:parser": "*" 105 | }, 106 | "name": "client", 107 | "sourceFiles": [ 108 | "source/graphql/client/codegen.d", 109 | "source/graphql/client/document.d", 110 | "source/graphql/client/query.d" 111 | ], 112 | "sourcePaths": [] 113 | }, 114 | { 115 | "dependencies": { 116 | "graphqld:client": "*", 117 | "vibe-d:data": ">=0.9.0", 118 | "vibe-d:http": ">=0.9.0" 119 | }, 120 | "name": "client-vibe", 121 | "sourceFiles": [ 122 | "source/graphql/client/vibe.d" 123 | ], 124 | "sourcePaths": [] 125 | } 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "diet-ng": "1.8.2", 5 | "eventcore": "0.9.35", 6 | "exceptionhandling": "1.0.0", 7 | "fixedsizearray": "1.3.0", 8 | "mir-linux-kernel": "1.0.1", 9 | "nullablestore": "2.1.0", 10 | "openssl": "3.3.4", 11 | "openssl-static": "1.0.5+3.0.8", 12 | "stdx-allocator": "2.77.5", 13 | "taggedalgebraic": "0.11.23", 14 | "vibe-container": "1.3.1", 15 | "vibe-core": "2.9.5", 16 | "vibe-d": "0.10.1", 17 | "vibe-http": "1.1.2", 18 | "vibe-inet": "1.0.0", 19 | "vibe-serialization": "1.0.5", 20 | "vibe-stream": "1.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /exeexcp: -------------------------------------------------------------------------------- 1 | 2 | @safe: 3 | class GQLDExecutionException : Exception { 4 | this(string msg, string f = __FILE__, size_t l = __LINE__) { 5 | super(msg, f, l); 6 | this.line = l; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql.yaml: -------------------------------------------------------------------------------- 1 | Document: 2 | Defi: [Definitions#defs] 3 | 4 | Definitions: 5 | Def: [Definition#def] 6 | Defs: [Definition#def, Definitions#follow] 7 | 8 | Definition: 9 | O: [OperationDefinition#op] 10 | F: [FragmentDefinition#frag] 11 | T: [TypeSystemDefinition#type] 12 | 13 | OperationDefinition: 14 | SelSet: [SelectionSet#ss] 15 | OT_N_VD: [OperationType#ot, name#name, VariableDefinitions#vd, 16 | Directives#d, SelectionSet#ss] 17 | OT_N_V: [OperationType#ot, name#name, VariableDefinitions#vd, 18 | SelectionSet#ss] 19 | OT_N_D: [OperationType#ot, name#name, Directives#d, SelectionSet#ss] 20 | OT_N: [OperationType#ot, name#name, SelectionSet#ss] 21 | OT_VD: [OperationType#ot, VariableDefinitions#vd, Directives#d, 22 | SelectionSet#ss] 23 | OT_V: [OperationType#ot, VariableDefinitions#vd, SelectionSet#ss] 24 | OT_D: [OperationType#ot, Directives#d, SelectionSet#ss] 25 | OT: [OperationType#ot, SelectionSet#ss] 26 | 27 | SelectionSet: 28 | SS: [lcurly, Selections#sel, rcurly] 29 | 30 | OperationType: 31 | Query: [query#tok] 32 | Mutation: [mutation#tok] 33 | Sub: [subscription#tok] 34 | 35 | Selections: 36 | Sel: [Selection#sel] 37 | Sels: [Selection#sel, Selections#follow] 38 | Selsc: [Selection#sel, comma, Selections#follow] 39 | 40 | Selection: 41 | Field: [Field#field] 42 | Spread: [dots, FragmentSpread#frag] 43 | IFrag: [dots, InlineFragment#ifrag] 44 | 45 | FragmentSpread: 46 | FD: [name#name, Directives#dirs] 47 | F: [name#name] 48 | 49 | InlineFragment: 50 | TDS: [on_, name#tc, Directives#dirs, SelectionSet#ss] 51 | TS: [on_, name#tc, SelectionSet#ss] 52 | DS: [Directives#dirs, SelectionSet#ss] 53 | S: [SelectionSet#ss] 54 | 55 | # Alias is build into FieldName 56 | Field: 57 | FADS: [FieldName#name, Arguments#args, Directives#dirs, 58 | SelectionSet#ss] 59 | FAS: [FieldName#name, Arguments#args, SelectionSet#ss] 60 | FAD: [FieldName#name, Arguments#args, Directives#dirs] 61 | FDS: [FieldName#name, Directives#dirs, SelectionSet#ss] 62 | FS: [FieldName#name, SelectionSet#ss] 63 | FD: [FieldName#name, Directives#dirs] 64 | FA: [FieldName#name, Arguments#args] 65 | F: [FieldName#name] 66 | 67 | FieldName: 68 | A: [Identifier#name, colon, name#aka] 69 | N: [Identifier#name] 70 | 71 | Identifier: 72 | I: [name#tok] 73 | "Mutation": [mutation#tok] 74 | "Subscription": [subscription#tok] 75 | "Scalar": [scalar#tok] 76 | "Schema": [schema#tok] 77 | "On": [on_#tok] 78 | "Directive": [directive#tok] 79 | "Enum": [enum_#tok] 80 | "Extend": [extend#tok] 81 | "Input": [input#tok] 82 | "Interface": [interface_#tok] 83 | "Implements": [implements#tok] 84 | "False": [false_#tok] 85 | "Fragment": [fragment#tok] 86 | "Query": [query#tok] 87 | "True": [true_#tok] 88 | "Type": [type#tok] 89 | "Null": [null_#tok] 90 | "Union": [union_#tok] 91 | 92 | Arguments: 93 | List: [lparen, ArgumentList#arg, rparen] 94 | Empty: [lparen, rparen] 95 | 96 | ArgumentList: 97 | A: [Argument#arg] 98 | ACS: [Argument#arg, comma, ArgumentList#follow] 99 | AS: [Argument#arg, ArgumentList#follow] 100 | 101 | Argument: 102 | Name: [name#name, colon, ValueOrVariable#vv] 103 | 104 | FragmentDefinition: 105 | FTDS: [fragment, name#name, on_, name#tc, Directives#dirs, 106 | SelectionSet#ss] 107 | FTS: [fragment, name#name, on_, name#tc, SelectionSet#ss] 108 | 109 | Directives: 110 | Dir: [Directive#dir] 111 | Dirs: [Directive#dir, Directives#follow] 112 | 113 | Directive: 114 | NArg: [at, name#name, Arguments#arg] 115 | N: [at, name#name] 116 | 117 | VariableDefinitions: 118 | Empty: [lparen, rparen] 119 | Vars: [lparen, VariableDefinitionList#vars, rparen] 120 | 121 | VariableDefinitionList: 122 | V: [VariableDefinition#var] 123 | VCF: [VariableDefinition#var, comma, VariableDefinitionList#follow] 124 | VF: [VariableDefinition#var, VariableDefinitionList#follow] 125 | 126 | VariableDefinition: 127 | VarD: [Variable#var, colon, Type#type, DefaultValue#dvalue] 128 | Var: [Variable#var, colon, Type#type] 129 | 130 | Variable: 131 | Var: [dollar, name#name] 132 | 133 | DefaultValue: 134 | DV: [equal, Value#value] 135 | 136 | ValueOrVariable: 137 | Val: [Value#val] 138 | Var: [Variable#var] 139 | 140 | Value: 141 | STR: [stringValue#tok] 142 | INT: [intValue#tok] 143 | FLOAT: [floatValue#tok] 144 | T: [true_#tok] 145 | F: [false_#tok] 146 | ARR: [Array#arr] 147 | O: [ObjectType#obj] 148 | E: [name#tok] 149 | N: [null_#tok] 150 | 151 | Type: 152 | TN: [name#tname, exclamation] 153 | LN: [ListType#list, exclamation] 154 | T: [name#tname] 155 | L: [ListType#list] 156 | 157 | ListType: 158 | T: [lbrack, Type#type, rbrack] 159 | 160 | Values: 161 | Val: [Value#val] 162 | Vals: [Value#val, comma, Values#follow] 163 | ValsNoComma: [Value#val, Values#follow] 164 | 165 | Array: 166 | Empty: [lbrack, rbrack] 167 | Value: [lbrack, Values#vals, rbrack] 168 | 169 | ObjectValues: 170 | V: [name#name, colon, ValueOrVariable#val] 171 | Vsc: [name#name, colon, ValueOrVariable#val, comma, ObjectValues#follow] 172 | Vs: [name#name, colon, ValueOrVariable#val, ObjectValues#follow] 173 | 174 | ObjectType: 175 | Var: [lcurly, ObjectValues#vals, rcurly] 176 | 177 | TypeSystemDefinition: 178 | S: [SchemaDefinition#sch] 179 | T: [TypeDefinition#td] 180 | TE: [TypeExtensionDefinition#ted] 181 | D: [DirectiveDefinition#dd] 182 | DS: [Description#des, SchemaDefinition#sch] 183 | DT: [Description#des, TypeDefinition#td] 184 | DTE: [Description#des, TypeExtensionDefinition#ted] 185 | DD: [Description#des, DirectiveDefinition#dd] 186 | 187 | TypeDefinition: 188 | S: [ScalarTypeDefinition#std] 189 | O: [ObjectTypeDefinition#otd] 190 | I: [InterfaceTypeDefinition#itd] 191 | U: [UnionTypeDefinition#utd] 192 | E: [EnumTypeDefinition#etd] 193 | IO: [InputObjectTypeDefinition#iod] 194 | 195 | SchemaDefinition: 196 | DO: [schema, Directives#dir, lcurly, OperationTypeDefinitions#otds, 197 | rcurly] 198 | O: [schema, lcurly, OperationTypeDefinitions#otds, rcurly] 199 | 200 | OperationTypeDefinitions: 201 | O: [OperationTypeDefinition#otd] 202 | OCS: [OperationTypeDefinition#otd, comma, 203 | OperationTypeDefinitions#follow] 204 | OS: [OperationTypeDefinition#otd, OperationTypeDefinitions#follow] 205 | 206 | OperationTypeDefinition: 207 | O: [OperationType#ot, colon, name#nt] 208 | 209 | ScalarTypeDefinition: 210 | D: [scalar, name#name, Directives#dir] 211 | S: [scalar, name#name] 212 | 213 | ObjectTypeDefinition: 214 | ID: [type, name#name, ImplementsInterfaces#ii, Directives#dir, 215 | lcurly, FieldDefinitions#fds, rcurly ] 216 | I: [type, name#name, ImplementsInterfaces#ii, lcurly, 217 | FieldDefinitions#fds, rcurly ] 218 | D: [type, name#name, Directives#dir, lcurly, FieldDefinitions#fds, 219 | rcurly ] 220 | F: [type, name#name, lcurly, FieldDefinitions#fds, rcurly ] 221 | 222 | FieldDefinitions: 223 | F: [FieldDefinition#fd] 224 | FC: [FieldDefinition#fd, comma, FieldDefinitions#follow] 225 | FNC: [FieldDefinition#fd, FieldDefinitions#follow] 226 | 227 | FieldDefinition: 228 | AD: [Identifier#name, ArgumentsDefinition#arg, colon, Type#typ, 229 | Directives#dir] 230 | A: [Identifier#name, ArgumentsDefinition#arg, colon, Type#typ] 231 | D: [Identifier#name, colon, Type#typ, Directives#dir] 232 | T: [Identifier#name, colon, Type#typ] 233 | DAD: [Description#des, Identifier#name, ArgumentsDefinition#arg, colon, Type#typ, 234 | Directives#dir] 235 | DA: [Description#des, Identifier#name, ArgumentsDefinition#arg, colon, Type#typ] 236 | DD: [Description#des, Identifier#name, colon, Type#typ, Directives#dir] 237 | DT: [Description#des, Identifier#name, colon, Type#typ] 238 | 239 | ImplementsInterfaces: 240 | N: [implements, NamedTypes#nts] 241 | 242 | NamedTypes: 243 | N: [name#name] 244 | NCS: [name#name, comma, NamedTypes#follow] 245 | NS: [name#name, NamedTypes#follow] 246 | 247 | ArgumentsDefinition: 248 | A: [lparen, InputValueDefinitions, rparen] 249 | NA: [lparen, rparen] 250 | 251 | InputValueDefinitions: 252 | I: [InputValueDefinition#iv] 253 | ICF: [InputValueDefinition#iv, comma, InputValueDefinitions#follow] 254 | IF: [InputValueDefinition#iv, InputValueDefinitions#follow] 255 | 256 | InputValueDefinition: 257 | TVD: [Identifier#name, colon, Type#type, DefaultValue#df, Directives#dirs] 258 | TD: [Identifier#name, colon, Type#type, Directives#dirs] 259 | TV: [Identifier#name, colon, Type#type, DefaultValue#df] 260 | T: [Identifier#name, colon, Type#type] 261 | DTVD: [Description#des, Identifier#name, colon, Type#type, DefaultValue#df, Directives#dirs] 262 | DTD: [Description#des, Identifier#name, colon, Type#type, Directives#dirs] 263 | DTV: [Description#des, Identifier#name, colon, Type#type, DefaultValue#df] 264 | DT: [Description#des, Identifier#name, colon, Type#type] 265 | 266 | InterfaceTypeDefinition: 267 | NDF: [interface_, name#name, Directives#dirs, lcurly, 268 | FieldDefinitions#fds, rcurly] 269 | NF: [interface_, name#name, lcurly, FieldDefinitions#fds, rcurly] 270 | 271 | UnionTypeDefinition: 272 | NDU: [union_, name#name, Directives#dirs, equal, UnionMembers#um] 273 | NU: [union_, name#name, equal, UnionMembers#um] 274 | 275 | UnionMembers: 276 | S: [name#name] 277 | SPF: [name#name, pipe, UnionMembers#follow] 278 | SF: [name#name, UnionMembers#follow] 279 | 280 | EnumTypeDefinition: 281 | NDE: [enum_, name#name, Directives#dir, lcurly, 282 | EnumValueDefinitions#evds, rcurly] 283 | NE: [enum_, name#name, lcurly, EnumValueDefinitions#evds, rcurly] 284 | 285 | EnumValueDefinitions: 286 | D: [EnumValueDefinition#evd] 287 | DCE: [EnumValueDefinition#evd, comma, EnumValueDefinitions#follow] 288 | DE: [EnumValueDefinition#evd, EnumValueDefinitions#follow] 289 | 290 | EnumValueDefinition: 291 | ED: [name#name, Directives#dirs] 292 | E: [name#name] 293 | DED: [Description#des, name#name, Directives#dirs] 294 | DE: [Description#des, name#name] 295 | 296 | TypeExtensionDefinition: 297 | O: [extend, ObjectTypeDefinition#otd] 298 | 299 | DirectiveDefinition: 300 | AD: [directive, at, name#name, ArgumentsDefinition#ad, on_, 301 | DirectiveLocations#dl] 302 | D: [directive, at, name#name, on_, DirectiveLocations#dl] 303 | 304 | DirectiveLocations: 305 | N: [name#name] 306 | NPF: [name#name, pipe, DirectiveLocations#follow] 307 | NF: [name#name, DirectiveLocations#follow] 308 | 309 | InputObjectTypeDefinition: 310 | NDI: [input, name#name, Directives#dirs, lcurly, 311 | InputValueDefinitions#ivds, rcurly] 312 | NI: [input, name#name, lcurly, InputValueDefinitions#ivds, rcurly] 313 | 314 | Description: 315 | S: [stringValue#tok] 316 | -------------------------------------------------------------------------------- /perftest/calc.d: -------------------------------------------------------------------------------- 1 | import std; 2 | 3 | string[] strs = 4 | [ "Command being timed:" 5 | , "Percent of CPU this job got:" 6 | ]; 7 | 8 | string[] floats = 9 | [ "User time (seconds):" 10 | , "System time (seconds):" 11 | ]; 12 | 13 | string[] floats2 = 14 | [ "Elapsed (wall clock) time (h:mm:ss or m:ss):" 15 | ]; 16 | 17 | string[] longs = 18 | [ "Average shared text size (kbytes):" 19 | , "Average unshared data size (kbytes):" 20 | , "Average stack size (kbytes):" 21 | , "Average total size (kbytes):" 22 | , "Maximum resident set size (kbytes):" 23 | , "Average resident set size (kbytes):" 24 | , "Major (requiring I/O) page faults:" 25 | , "Minor (reclaiming a frame) page faults:" 26 | , "Voluntary context switches:" 27 | , "Involuntary context switches:" 28 | , "Swaps:" 29 | , "File system inputs:" 30 | , "File system outputs:" 31 | , "Socket messages sent:" 32 | , "Socket messages received:" 33 | , "Signals delivered:" 34 | , "Page size (bytes):" 35 | , "Exit status:" 36 | ]; 37 | 38 | string[] perf = 39 | [ "cache-references:u" 40 | , "cache-misses:u" 41 | , "cycles:u" 42 | , "instructions:u" 43 | , "branches:u" 44 | , "faults:u" 45 | , "migrations:u" 46 | , "L1-dcache-loads:u" 47 | , "L1-dcache-load-misses:u" 48 | , "ic_tag_hit_miss.all_instruction_cache_accesses:u" 49 | ]; 50 | 51 | double wallClock(string s) { 52 | s = s.strip(); 53 | s = s[3 .. $]; 54 | return to!(double)(s); 55 | } 56 | 57 | void main(string[] args) { 58 | long[string] longV; 59 | double[string] floatV; 60 | 61 | string[] lines = readText("out") 62 | .splitter("\n") 63 | .map!(l => l.strip()) 64 | .filter!(l => !l.empty) 65 | .array; 66 | o: foreach(l; lines) { 67 | foreach(p; perf) { 68 | ptrdiff_t i = l.indexOf(p); 69 | if(i != -1) { 70 | long v = l[0 .. i].strip().to!(long)(); 71 | if(p in longV) { 72 | longV[p] += v; 73 | } else { 74 | longV[p] = v; 75 | } 76 | continue o; 77 | } 78 | } 79 | foreach(p; longs) { 80 | ptrdiff_t i = l.indexOf(p); 81 | if(i != -1) { 82 | long v = l[i + p.length .. $].strip().to!(long)(); 83 | if(p in longV) { 84 | longV[p] += v; 85 | } else { 86 | longV[p] = v; 87 | } 88 | continue o; 89 | } 90 | } 91 | foreach(string p; floats) { 92 | ptrdiff_t i = l.indexOf(p); 93 | if(i != -1) { 94 | string s = l[0 .. i].strip(); 95 | if(s.empty) { 96 | continue o; 97 | } 98 | double v = s.to!(double)(); 99 | if(p in floatV) { 100 | floatV[p] += v; 101 | } else { 102 | floatV[p] = v; 103 | } 104 | continue o; 105 | } 106 | } 107 | foreach(string p; floats2) { 108 | ptrdiff_t i = l.indexOf(p); 109 | if(i != -1) { 110 | string s = l[p.length .. $].strip(); 111 | if(s.empty) { 112 | continue o; 113 | } 114 | double v = wallClock(s); 115 | if(p in floatV) { 116 | floatV[p] += v; 117 | } else { 118 | floatV[p] = v; 119 | } 120 | continue o; 121 | } 122 | } 123 | } 124 | JSONValue j; 125 | foreach(k, v; longV) { 126 | j[k] = v; 127 | } 128 | foreach(k, v; floatV) { 129 | j[k] = v; 130 | } 131 | j["l1misses"] = ((cast(double)longV["L1-dcache-load-misses:u"] / 132 | longV["L1-dcache-loads:u"]) * 100); 133 | j["cache_misses"] = ((cast(double)longV["cache-misses:u"] / 134 | longV["cache-references:u"]) * 100); 135 | auto f = File(args[1], "w"); 136 | f.writeln(j.toPrettyString()); 137 | } 138 | -------------------------------------------------------------------------------- /perftest/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Robert Schadek" 4 | ], 5 | "dependencies": { 6 | "graphqld": { "path" : "../" } 7 | }, 8 | "copyright": "Copyright © 2024, Robert Schadek", 9 | "description": "A minimal D application.", 10 | "license": "GPL-3.0-or-later", 11 | "name": "perftest" 12 | } 13 | -------------------------------------------------------------------------------- /perftest/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "diet-ng": "1.8.3", 5 | "eventcore": "0.9.35", 6 | "exceptionhandling": "1.0.0", 7 | "fixedsizearray": "1.3.0", 8 | "graphqld": {"path":"../"}, 9 | "mir-linux-kernel": "1.2.1", 10 | "nullablestore": "2.1.0", 11 | "openssl": "3.3.4", 12 | "openssl-static": "1.0.5+3.0.8", 13 | "stdx-allocator": "2.77.5", 14 | "taggedalgebraic": "0.11.23", 15 | "vibe-container": "1.4.1", 16 | "vibe-core": "2.9.6", 17 | "vibe-d": "0.10.1", 18 | "vibe-http": "1.2.1", 19 | "vibe-inet": "1.1.0", 20 | "vibe-serialization": "1.0.7", 21 | "vibe-stream": "1.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /perftest/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | for i in $(seq 1 10); 4 | do 5 | /usr/bin/time -v perf stat -B -e cache-references,cache-misses,cycles,instructions,branches,faults,migrations,L1-dcache-loads,L1-dcache-load-misses,ic_tag_hit_miss.all_instruction_cache_accesses ./perftest 6 | done 7 | -------------------------------------------------------------------------------- /perftest/schema.gql: -------------------------------------------------------------------------------- 1 | schema { 2 | mutation: mutationType 3 | query: queryType 4 | subscription: subscriptionType 5 | } 6 | type mutationType { 7 | getStupidestCrewman: Character! 8 | addCrewman(arg: AddCrewmanData!): Character! 9 | name: String! 10 | } 11 | type queryType { 12 | shipsselection(ids: [Int!]!): [Starship!]! 13 | characters(series: Series!): [Character!]! 14 | starships(overSize: Float!): [Starship!]! 15 | captain(series: Series!): Character! 16 | humanoids: [Humanoid!]! 17 | currentTime: DateTime! 18 | starship(id: Int!): Starship 19 | starshipSimple2(id: Int!): StarshipSimple2 20 | name: String! 21 | androids: [Android!]! 22 | resolverWillThrow: [Android!]! 23 | numberBetween(searchInput: InputIn!): Starship! 24 | search(name: String!): SearchResult! @deprecated(reason: "To complex") 25 | starshipSimple3(id: Int!): StarshipSimple3 26 | starshipSimple(id: Int!): StarshipSimple 27 | starshipDoesNotExist: Starship! 28 | character(id: Int!): Character 29 | alwaysEmpty: [Starship!] 30 | } 31 | type StarshipSimple { 32 | commander: Character! 33 | } 34 | type subscriptionType { 35 | starships: [Starship!]! 36 | name: String! 37 | } 38 | type Android implements Character { 39 | isDead: Boolean! 40 | ships: Starship 41 | series: [Series!]! 42 | id: Int! 43 | ship: Starship 44 | primaryFunction: String! 45 | name: String! 46 | allwaysNull: Starship 47 | commands: [Character!]! 48 | alsoAllwaysNull: Int 49 | someOldField: Int! @deprecated(reason: "Stupid name") 50 | commanders: [Character!]! 51 | } 52 | enum Series { 53 | TheOriginalSeries, 54 | TheNextGeneration, 55 | DeepSpaceNine, 56 | Voyager, 57 | Enterprise, 58 | Discovery 59 | } 60 | 61 | type Input { 62 | first: Int! 63 | after: String 64 | } 65 | input InputIn { 66 | first: Int! 67 | after: String 68 | } 69 | type Starship { 70 | series: [Series]! 71 | id: Int! 72 | commander: Character! 73 | name: String! 74 | size: Float! 75 | crew: [Character!]! 76 | designation: String! 77 | } 78 | type StarshipSimple2 { 79 | id: Int! 80 | } 81 | type StarshipSimple3 { 82 | series: [Series]! 83 | } 84 | interface Character { 85 | isDead: Boolean! 86 | ships: Starship 87 | series: [Series!]! 88 | id: Int! 89 | ship: Starship 90 | name: String! 91 | allwaysNull: Starship 92 | commands: [Character!]! 93 | alsoAllwaysNull: Int 94 | someOldField: Int! @deprecated(reason: "Stupid name") 95 | commanders: [Character!]! 96 | } 97 | input AddCrewmanData { 98 | shipId: Int! 99 | location: Vector! 100 | series: [Series!]! 101 | name: String! 102 | } 103 | input Vector { 104 | y: Float! 105 | x: Float! 106 | } 107 | type Humanoid implements Character { 108 | isDead: Boolean! 109 | ships: Starship 110 | series: [Series!]! 111 | id: Int! 112 | ship: Starship 113 | dateOfBirth: Date! 114 | name: String! 115 | species: String! 116 | allwaysNull: Starship 117 | commands: [Character!]! 118 | alsoAllwaysNull: Int 119 | someOldField: Int! @deprecated(reason: "Stupid name") 120 | commanders: [Character!]! 121 | } 122 | scalar Date 123 | union SearchResult = Humanoid | Android | Starship 124 | scalar DateTime 125 | -------------------------------------------------------------------------------- /perftest/source/app.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.file : readText; 3 | 4 | import vibe.vibe; 5 | import vibe.core.core; 6 | import vibe.data.json; 7 | 8 | import graphql.parser; 9 | import graphql.builder; 10 | import graphql.lexer; 11 | import graphql.ast; 12 | 13 | import graphql.helper; 14 | import graphql.schema; 15 | import graphql.traits; 16 | import graphql.argumentextractor; 17 | import graphql.schema.toschemafile; 18 | import graphql.exception; 19 | import graphql.graphql; 20 | import graphql.testschema; 21 | 22 | import countvisitor; 23 | 24 | pragma(mangle, "_D4core8internal4hash__T6hashOfTAxS7graphql6schema18introspectiontypes6__TypeZQCcFNbNfQCcmZm") 25 | ulong __fun() { 26 | return 0; 27 | } 28 | 29 | void main() { 30 | string toParse = readText("schema.docs.graphql"); 31 | foreach(i; 0 .. 50) { 32 | auto l = Lexer(toParse, QueryParser.no); 33 | //auto l = Lexer(toParse); 34 | auto p = Parser(l); 35 | uint d = p.parseDocument(); 36 | auto cv = new CountVisitor(&p); 37 | cv.accept(p.documents[0]); 38 | doNotOptimizeAway(cv.countsToString()); 39 | } 40 | } 41 | 42 | void doNotOptimizeAway(T...)(auto ref T t) 43 | { 44 | foreach (ref it; t) 45 | { 46 | doNotOptimizeAwayImpl(&it); 47 | } 48 | } 49 | 50 | private void doNotOptimizeAwayImpl(void* p) 51 | { 52 | import core.thread : getpid; 53 | import std.stdio : writeln; 54 | 55 | if (getpid() == 0) 56 | { 57 | writeln(p); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /perftest/source/countvisitor.d: -------------------------------------------------------------------------------- 1 | module countvisitor; 2 | 3 | import std.traits : Unqual, Parameters; 4 | import std.conv : to; 5 | import std.stdio; 6 | import std.string : indexOf; 7 | import std.format; 8 | 9 | import graphql.ast; 10 | import graphql.visitor; 11 | import graphql.tokenmodule; 12 | import graphql.parser; 13 | 14 | class CountVisitor : Visitor { 15 | alias accept = Visitor.accept; 16 | alias enter = Visitor.enter; 17 | alias exit = Visitor.exit; 18 | 19 | mixin(genCountTables()); 20 | mixin(genCountFunction()); 21 | 22 | this(Parser* parser) { 23 | super(parser); 24 | } 25 | } 26 | 27 | string genCountFunction() { 28 | string ret; 29 | string[] all; 30 | static foreach(it; __traits(getOverloads, Visitor, "accept")) {{ 31 | alias Params = Parameters!(it); 32 | enum pName = Params[0].stringof; 33 | static if(pName.indexOf("const(") == -1) { 34 | ret ~= format(` 35 | override void accept(ref %1$s f) { 36 | super.accept(f); 37 | this.%1$sCounter++; 38 | } 39 | 40 | override void enter(ref %1$s op) { 41 | this.%1$sCounterEnter++; 42 | } 43 | 44 | override void exit(ref %1$s op) { 45 | this.%1$sCounterExit++; 46 | } 47 | 48 | `, pName); 49 | } 50 | }} 51 | return ret; 52 | } 53 | 54 | string genCountTables() { 55 | string ret; 56 | string[] all; 57 | static foreach(it; __traits(getOverloads, Visitor, "accept")) {{ 58 | alias Params = Parameters!(it); 59 | enum pName = Params[0].stringof; 60 | static if(pName.indexOf("const(") == -1) { 61 | all ~= pName; 62 | ret ~= "\tlong " ~ Params[0].stringof ~ "Counter;\n"; 63 | ret ~= "\tlong " ~ Params[0].stringof ~ "CounterEnter;\n"; 64 | ret ~= "\tlong " ~ Params[0].stringof ~ "CounterExit;\n"; 65 | } 66 | }} 67 | 68 | ret ~= ` 69 | string countsToString() { 70 | string ret; 71 | `; 72 | foreach(it; all) { 73 | ret ~= "\t\tret ~= \"" ~ it ~ "Counter = \" ~ to!string(" ~ it 74 | ~ "Counter)" ~ " ~ \"\\n\";\n"; 75 | ret ~= "\t\tret ~= \"" ~ it ~ "CounterEnter = \" ~ to!string(" ~ it 76 | ~ "CounterEnter)" ~ " ~ \"\\n\";\n"; 77 | ret ~= "\t\tret ~= \"" ~ it ~ "CounterExit = \" ~ to!string(" ~ it 78 | ~ "CounterExit)" ~ " ~ \"\\n\";\n"; 79 | } 80 | ret ~= ` 81 | return ret; 82 | } 83 | `; 84 | return ret; 85 | } 86 | 87 | unittest { 88 | auto c = new ConstVisitor(); 89 | } 90 | -------------------------------------------------------------------------------- /source/graphql/argumentextractor.d: -------------------------------------------------------------------------------- 1 | module graphql.argumentextractor; 2 | 3 | import std.array : back, empty, popBack; 4 | import std.conv : to; 5 | import std.format : format; 6 | import std.exception : enforce; 7 | import std.stdio : writefln; 8 | 9 | import vibe.data.json; 10 | 11 | import graphql.visitor; 12 | import graphql.ast; 13 | import graphql.builder : FieldRangeItem; 14 | 15 | 16 | @safe: 17 | 18 | Json getArguments(Selections sels, Json variables) { 19 | //writefln("%s", variables); 20 | auto ae = new ArgumentExtractor(variables); 21 | ae.accept(cast(const(Selections))sels); 22 | return ae.arguments; 23 | } 24 | 25 | Json getArguments(InlineFragment ilf, Json variables) { 26 | auto ae = new ArgumentExtractor(variables); 27 | ae.accept(cast(const(InlineFragment))ilf); 28 | return ae.arguments; 29 | } 30 | 31 | Json getArguments(FragmentSpread fs, Json variables) { 32 | auto ae = new ArgumentExtractor(variables); 33 | ae.accept(cast(const(FragmentSpread))fs); 34 | return ae.arguments; 35 | } 36 | 37 | Json getArguments(const(Directive) dir, Json variables) { 38 | auto ae = new ArgumentExtractor(variables); 39 | ae.accept(dir); 40 | return ae.arguments; 41 | } 42 | 43 | Json getArguments(FieldRangeItem item, Json variables) { 44 | auto ae = new ArgumentExtractor(variables); 45 | ae.accept(cast(const(Field))item.f); 46 | return ae.arguments; 47 | } 48 | 49 | Json getArguments(Field field, Json variables) { 50 | auto ae = new ArgumentExtractor(variables); 51 | ae.accept(cast(const(Field))field); 52 | return ae.arguments; 53 | } 54 | 55 | class ArgumentExtractor : ConstVisitor { 56 | alias enter = ConstVisitor.enter; 57 | alias exit = ConstVisitor.exit; 58 | alias accept = ConstVisitor.accept; 59 | 60 | Json arguments; 61 | Json variables; 62 | 63 | string[] curNames; 64 | 65 | this(Json variables) { 66 | this.variables = variables; 67 | this.arguments = Json.emptyObject(); 68 | } 69 | 70 | void assign(Json toAssign) @trusted { 71 | Json* arg = &this.arguments; 72 | //logf("%(%s.%) %s %s", this.curNames, this.arguments, toAssign); 73 | assert(!this.curNames.empty); 74 | foreach(idx; 0 .. this.curNames.length - 1) { 75 | enforce(arg !is null); 76 | arg = &((*arg)[this.curNames[idx]]); 77 | } 78 | 79 | enforce(arg !is null); 80 | 81 | if(this.curNames.back in (*arg) 82 | && ((*arg)[this.curNames.back]).type == Json.Type.array) 83 | { 84 | ((*arg)[this.curNames.back]) ~= toAssign; 85 | } else if((*arg).type == Json.Type.object) { 86 | ((*arg)[this.curNames.back]) = toAssign; 87 | } else { 88 | ((*arg)[this.curNames.back]) = toAssign; 89 | } 90 | //logf("%s", this.arguments); 91 | } 92 | 93 | override void accept(const(Field) obj) { 94 | final switch(obj.ruleSelection) { 95 | case FieldEnum.FADS: 96 | obj.args.visit(this); 97 | obj.dirs.visit(this); 98 | break; 99 | case FieldEnum.FAS: 100 | obj.args.visit(this); 101 | break; 102 | case FieldEnum.FAD: 103 | obj.args.visit(this); 104 | obj.dirs.visit(this); 105 | break; 106 | case FieldEnum.FDS: 107 | obj.dirs.visit(this); 108 | break; 109 | case FieldEnum.FS: 110 | break; 111 | case FieldEnum.FD: 112 | obj.dirs.visit(this); 113 | break; 114 | case FieldEnum.FA: 115 | obj.args.visit(this); 116 | break; 117 | case FieldEnum.F: 118 | break; 119 | } 120 | } 121 | 122 | override void enter(const(Argument) arg) { 123 | this.curNames ~= arg.name.value; 124 | } 125 | 126 | override void exit(const(Argument) arg) { 127 | this.curNames.popBack(); 128 | } 129 | 130 | override void accept(const(ValueOrVariable) obj) { 131 | import graphql.validation.exception : VariablesUseException; 132 | final switch(obj.ruleSelection) { 133 | case ValueOrVariableEnum.Val: 134 | obj.val.visit(this); 135 | break; 136 | case ValueOrVariableEnum.Var: 137 | string varName = obj.var.name.value; 138 | enforce!VariablesUseException(varName in this.variables, 139 | format("Variable with name '%s' required available '%s'", 140 | varName, this.variables) 141 | ); 142 | //writefln("%s %s", varName, this.variables); 143 | this.assign(this.variables[varName]); 144 | break; 145 | } 146 | } 147 | 148 | override void accept(const(ObjectValues) obj) { 149 | enter(obj); 150 | final switch(obj.ruleSelection) { 151 | case ObjectValuesEnum.V: 152 | this.curNames ~= obj.name.value; 153 | obj.val.visit(this); 154 | this.curNames.popBack(); 155 | break; 156 | case ObjectValuesEnum.Vsc: 157 | this.curNames ~= obj.name.value; 158 | obj.val.visit(this); 159 | this.curNames.popBack(); 160 | obj.follow.visit(this); 161 | break; 162 | case ObjectValuesEnum.Vs: 163 | this.curNames ~= obj.name.value; 164 | obj.val.visit(this); 165 | this.curNames.popBack(); 166 | obj.follow.visit(this); 167 | break; 168 | } 169 | exit(obj); 170 | } 171 | 172 | override void enter(const(Value) val) { 173 | final switch(val.ruleSelection) { 174 | case ValueEnum.STR: 175 | this.assign(Json(val.tok.value)); 176 | break; 177 | case ValueEnum.INT: 178 | this.assign(Json(to!long(val.tok.value))); 179 | break; 180 | case ValueEnum.FLOAT: 181 | this.assign(Json(to!double(val.tok.value))); 182 | break; 183 | case ValueEnum.T: 184 | this.assign(Json(true)); 185 | break; 186 | case ValueEnum.F: 187 | this.assign(Json(false)); 188 | break; 189 | case ValueEnum.ARR: 190 | this.assign(Json.emptyArray()); 191 | break; 192 | case ValueEnum.O: 193 | this.assign(Json.emptyObject()); 194 | break; 195 | case ValueEnum.E: 196 | this.assign(Json(val.tok.value)); 197 | break; 198 | case ValueEnum.N: 199 | this.assign(Json(null)); 200 | break; 201 | } 202 | } 203 | } 204 | 205 | import graphql.helper : lexAndParse; 206 | 207 | unittest { 208 | string s = ` 209 | { 210 | starships(overSize: 10) { 211 | name 212 | } 213 | } 214 | `; 215 | 216 | const auto d = lexAndParse(s); 217 | } 218 | -------------------------------------------------------------------------------- /source/graphql/argumentextractortests.d: -------------------------------------------------------------------------------- 1 | module graphql.argumentextractortests; 2 | 3 | import std.format : format; 4 | 5 | import vibe.data.json; 6 | 7 | import graphql.ast; 8 | import graphql.astselector; 9 | import graphql.helper : lexAndParse; 10 | import graphql.argumentextractor; 11 | import graphql.testschema; 12 | import graphql.graphql; 13 | 14 | unittest { 15 | string q = ` 16 | query a($s: boolean, $after: String) { 17 | starships(overSize: 10) { 18 | name 19 | crew @skip(if: $s) { 20 | ...hyooman 21 | ...robot 22 | ...charac 23 | } 24 | } 25 | numberBetween(searchInput: 26 | { first: 10 27 | , after: $after 28 | } 29 | ) { 30 | id 31 | } 32 | } 33 | 34 | fragment hyooman on Humanoid { 35 | species 36 | dateOfBirth 37 | } 38 | 39 | fragment robot on Android { 40 | primaryFunction 41 | } 42 | 43 | fragment charac on Character { 44 | ...robot 45 | id 46 | ...hyooman 47 | name(get: $s) 48 | series 49 | }`; 50 | 51 | Json vars = parseJsonString(`{ "s": false, "after": "Hello World" }`); 52 | 53 | Document doc = cast()lexAndParse(q); 54 | assert(doc !is null); 55 | 56 | { 57 | auto startships = astSelect!Field(doc, "a.starships"); 58 | assert(startships !is null); 59 | 60 | Json args = getArguments(cast()startships, vars); 61 | assert(args == parseJsonString(`{"overSize" : 10}`), 62 | format("%s", args) 63 | ); 64 | } 65 | 66 | { 67 | auto crew = astSelect!Field(doc, "a.starships.crew"); 68 | assert(crew !is null); 69 | 70 | Json args = getArguments(cast()crew, vars); 71 | assert(args == parseJsonString(`{"if" : false}`), format("%s", args)); 72 | } 73 | 74 | { 75 | auto name = astSelect!Field(doc, "a.starships.crew.name"); 76 | assert(name !is null); 77 | 78 | Json args = getArguments(cast()name, vars); 79 | assert(args == parseJsonString(`{"get" : false}`), format("%s", args)); 80 | } 81 | 82 | { 83 | auto searchInput = astSelect!Field(doc, "a.numberBetween"); 84 | assert(searchInput !is null); 85 | 86 | Json args = getArguments(cast()searchInput, vars); 87 | assert(args == parseJsonString( 88 | `{ "searchInput": {"first" : 10, "after": "Hello World"}}`), 89 | format("%s", args)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /source/graphql/astselector.d: -------------------------------------------------------------------------------- 1 | module graphql.astselector; 2 | 3 | import std.array : back, empty, front, popBack; 4 | import std.exception : enforce; 5 | 6 | import graphql.tokenmodule; 7 | import graphql.ast; 8 | import graphql.visitor : ConstVisitor; 9 | 10 | /* Sometimes you need to select a specific part of the ast, 11 | this module allows to do that. 12 | */ 13 | 14 | @safe: 15 | 16 | const(T) astSelect(T,S)(const(S) input, string path) { 17 | auto astsel = new AstSelector(path); 18 | static if(is(S == Document)) { 19 | return astsel.get!T(input, input); 20 | } else { 21 | return astsel.get!T(input); 22 | } 23 | } 24 | 25 | const(T) astSelect(T,S)(const(S) input, const(Document) doc, string path) { 26 | auto astsel = new AstSelector(path); 27 | return astsel.get!T(input, doc); 28 | } 29 | 30 | class AstSelector : ConstVisitor { 31 | import std.format : format; 32 | import std.typecons : rebindable, Rebindable; 33 | alias enter = ConstVisitor.enter; 34 | alias exit = ConstVisitor.exit; 35 | alias accept = ConstVisitor.accept; 36 | 37 | Rebindable!(const(Document)) document; 38 | 39 | const(string[]) sp; 40 | size_t spPos; 41 | 42 | Rebindable!(const(Node)) result; 43 | Rebindable!(const(Node))[] stack; 44 | 45 | this(string p) { 46 | import std.string : split; 47 | this.sp = p.split('.'); 48 | } 49 | 50 | const(T) get(T,S)(const(S) input, const(Document) doc) { 51 | this.document = doc; 52 | this.accept(input); 53 | return cast(typeof(return))this.result.get(); 54 | } 55 | 56 | const(T) get(T,S)(const(S) input) { 57 | return this.get!T(input, null); 58 | } 59 | 60 | bool takeName(string name, const(Node) nn) { 61 | if(this.spPos < this.sp.length && name == this.sp[this.spPos]) { 62 | this.stack ~= rebindable(nn); 63 | } else { 64 | return false; 65 | } 66 | 67 | ++this.spPos; 68 | if(this.spPos == this.sp.length) { 69 | enforce(this.result.get() is null); 70 | this.result = this.stack.back; 71 | } 72 | return true; 73 | } 74 | 75 | void popStack(bool shouldPop) { 76 | enforce(this.stack.length == this.spPos, 77 | format("stack.length %s, spPos %s", this.stack.length, 78 | this.spPos)); 79 | enforce(shouldPop ? this.stack.length > 0 : true, 80 | "should pop put stack is empty"); 81 | if(shouldPop) { 82 | this.stack.popBack(); 83 | --this.spPos; 84 | } 85 | } 86 | 87 | override void accept(const(OperationDefinition) obj) { 88 | if(obj.name.type != TokenType.undefined) { 89 | immutable bool shouldPop = this.takeName(obj.name.value, obj); 90 | if(shouldPop) { 91 | if(obj.vd !is null) { 92 | obj.vd.visit(this); 93 | } 94 | if(obj.d !is null) { 95 | obj.d.visit(this); 96 | } 97 | if(obj.ss !is null) { 98 | obj.ss.visit(this); 99 | } 100 | } 101 | this.popStack(shouldPop); 102 | } 103 | } 104 | 105 | override void accept(const(Field) obj) { 106 | bool shouldPop; 107 | 108 | scope(exit) { 109 | this.popStack(shouldPop); 110 | } 111 | 112 | shouldPop = this.takeName(obj.name.name.tok.value, obj); 113 | 114 | if(shouldPop) { 115 | if(obj.args !is null) { 116 | obj.args.visit(this); 117 | } 118 | if(obj.dirs !is null) { 119 | obj.dirs.visit(this); 120 | } 121 | if(obj.ss !is null) { 122 | obj.ss.visit(this); 123 | } 124 | } 125 | } 126 | 127 | override void accept(const(InlineFragment) obj) { 128 | if(obj.tc.type != TokenType.undefined) { 129 | immutable bool shouldPop = this.takeName(obj.tc.value, obj); 130 | if(shouldPop) { 131 | if(obj.dirs !is null) { 132 | obj.dirs.visit(this); 133 | } 134 | if(obj.ss !is null) { 135 | obj.ss.visit(this); 136 | } 137 | } 138 | this.popStack(shouldPop); 139 | } 140 | } 141 | 142 | override void accept(const(FragmentSpread) fragSpread) { 143 | import graphql.builder : findFragment; 144 | const(FragmentDefinition) frag = findFragment(this.document.get(), 145 | fragSpread.name.value 146 | ); 147 | immutable bool shouldPop = this.takeName(fragSpread.name.value, frag); 148 | frag.visit(this); 149 | this.popStack(shouldPop); 150 | } 151 | } 152 | 153 | unittest { 154 | string s = ` 155 | query foo { 156 | a 157 | }`; 158 | 159 | auto d = lexAndParse(s); 160 | auto foo = d.astSelect!OperationDefinition("foo"); 161 | assert(foo !is null); 162 | assert(foo.name.value == "foo"); 163 | } 164 | 165 | unittest { 166 | string s = ` 167 | query foo { 168 | a 169 | } 170 | 171 | mutation bar { 172 | b 173 | } 174 | 175 | `; 176 | 177 | auto d = lexAndParse(s); 178 | auto bar = d.astSelect!OperationDefinition("bar"); 179 | assert(bar !is null); 180 | assert(bar.name.value == "bar"); 181 | } 182 | 183 | unittest { 184 | string s = ` 185 | query foo { 186 | a { 187 | b 188 | } 189 | } 190 | 191 | `; 192 | 193 | auto d = lexAndParse(s); 194 | auto a = d.astSelect!Field("foo.a"); 195 | assert(a !is null); 196 | assert(a.name.name.tok.value == "a"); 197 | } 198 | 199 | unittest { 200 | string s = ` 201 | query foo { 202 | a { 203 | b 204 | } 205 | } 206 | 207 | `; 208 | 209 | auto d = lexAndParse(s); 210 | auto a = d.astSelect!Document("foo.a"); 211 | assert(a is null); 212 | } 213 | 214 | unittest { 215 | string s = ` 216 | query foo { 217 | a { 218 | b 219 | } 220 | } 221 | 222 | mutation a { 223 | foo { 224 | b 225 | } 226 | } 227 | 228 | `; 229 | 230 | auto d = lexAndParse(s); 231 | auto foo = d.astSelect!Field("a.foo"); 232 | assert(foo !is null); 233 | } 234 | 235 | unittest { 236 | string s = ` 237 | query foo { 238 | a { 239 | b 240 | } 241 | c { 242 | b 243 | } 244 | } 245 | 246 | mutation a { 247 | foo { 248 | b 249 | } 250 | } 251 | 252 | `; 253 | 254 | auto d = lexAndParse(s); 255 | auto foo = d.astSelect!Field("foo.a.b"); 256 | assert(foo !is null); 257 | assert(foo.name.name.tok.value == "b"); 258 | } 259 | 260 | unittest { 261 | string s = ` 262 | fragment Foo on Bar { 263 | a @skip(if: true) 264 | } 265 | 266 | query foo { 267 | ...Foo 268 | } 269 | 270 | `; 271 | 272 | auto d = lexAndParse(s); 273 | auto a = d.astSelect!Field("foo.a"); 274 | assert(a !is null); 275 | assert(a.dirs !is null); 276 | assert(a.dirs.dir.name.value == "skip"); 277 | } 278 | 279 | import std.range : take; 280 | import graphql.helper : lexAndParse; 281 | 282 | struct RandomPaths { 283 | import std.random : choice, Random; 284 | import std.algorithm.sorting : sort; 285 | import std.algorithm.iteration : uniq, splitter, map, joiner; 286 | import std.conv : to; 287 | import std.range : take, iota; 288 | import std.array : array; 289 | import std.ascii : isWhite; 290 | import std.string : strip, split; 291 | string[] elems; 292 | string toIgnore; 293 | size_t len; 294 | 295 | string front; 296 | Random rnd; 297 | 298 | static RandomPaths opCall(string elems, string toIgnore, uint seed) { 299 | RandomPaths ret; 300 | 301 | ret.elems = elems.splitter!isWhite() 302 | .map!(e => e.strip) 303 | .array 304 | .sort 305 | .uniq 306 | .array; 307 | 308 | ret.toIgnore = toIgnore; 309 | ret.rnd = Random(seed); 310 | ret.len = toIgnore.split('.').length; 311 | return ret; 312 | } 313 | 314 | private void build() { 315 | do { 316 | this.front = iota(this.len) 317 | .map!(i => choice(this.elems, this.rnd)) 318 | .joiner(".") 319 | .to!string(); 320 | } while(this.front == this.toIgnore); 321 | } 322 | 323 | void popFront() { 324 | this.build(); 325 | } 326 | 327 | enum bool empty = false; 328 | } 329 | 330 | unittest { 331 | import std.array : array; 332 | import std.algorithm.sorting : sort; 333 | import std.algorithm.iteration : uniq; 334 | import std.format : format; 335 | 336 | string s = ` query foo { a { b } c { b } foo ( args : 10 ) { ... on Foo { 337 | bar } } } mutation a { foo @ ship ( if : true) { b } } `; 338 | string toIgnore = "foo.a.b"; 339 | 340 | string[] r = take(RandomPaths(s, toIgnore, 1337), 10).array.sort.release; 341 | string[] su = r.dup.sort.uniq.array; 342 | assert(r == su, format("\n%s\n%s", r, su)); 343 | } 344 | 345 | unittest { 346 | string s = ` 347 | query foo { 348 | a { 349 | b 350 | } 351 | c { 352 | b 353 | } 354 | foo ( args : 10 ) { 355 | ... on Foo { 356 | bar 357 | } 358 | } 359 | } 360 | 361 | mutation a { 362 | foo @ ship ( if : true) { 363 | b 364 | } 365 | } 366 | `; 367 | 368 | auto d = lexAndParse(s); 369 | string toIgnore = "foo.a.b"; 370 | const(Field) foo = astSelect!Field(d, toIgnore); 371 | foreach(string p; take(RandomPaths(s, toIgnore, 1337), 5000)) { 372 | auto bar = astSelect!Field(d, p); 373 | assert(bar is null || bar !is foo); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /source/graphql/builder.d: -------------------------------------------------------------------------------- 1 | module graphql.builder; 2 | 3 | version(LDC) { 4 | import std.experimental.logger : logf; 5 | } else { 6 | import std.logger : logf; 7 | } 8 | import std.array : back, empty; 9 | import std.exception : enforce; 10 | import std.format : format; 11 | import std.typecons : Flag; 12 | 13 | import vibe.data.json; 14 | 15 | import fixedsizearray; 16 | 17 | import graphql.argumentextractor; 18 | import graphql.helper; 19 | import graphql.ast; 20 | import graphql.parser; 21 | import graphql.lexer; 22 | import graphql.directives; 23 | 24 | @safe: 25 | 26 | const(FragmentDefinition) findFragment(const(Document) doc, string name) { 27 | import std.algorithm.searching : canFind; 28 | enforce(doc !is null); 29 | const(Definitions) cur = doc.defs; 30 | return findFragmentImpl(cur, name); 31 | } 32 | 33 | const(FragmentDefinition) findFragmentImpl(const(Definitions) cur, 34 | string name) 35 | { 36 | if(cur is null) { 37 | return null; 38 | } else { 39 | enforce(cur.def !is null); 40 | if(cur.def.ruleSelection == DefinitionEnum.F) { 41 | enforce(cur.def.frag !is null); 42 | if(cur.def.frag.name.value == name) { 43 | return cur.def.frag; 44 | } 45 | } 46 | return findFragmentImpl(cur.follow, name); 47 | } 48 | } 49 | 50 | Selections findFragment(Document doc, string name, string[] typenames) { 51 | import std.algorithm.searching : canFind; 52 | if(doc is null) { 53 | return null; 54 | } 55 | Definitions cur = doc.defs; 56 | while(cur !is null) { 57 | enforce(cur.def !is null); 58 | if(cur.def.ruleSelection == DefinitionEnum.F) { 59 | enforce(cur.def.frag !is null); 60 | //logf("%s == %s && %s in %s", cur.def.frag.name.value, name, 61 | // cur.def.frag.tc.value, typenames 62 | // ); 63 | if(cur.def.frag.name.value == name 64 | && canFind(typenames, cur.def.frag.tc.value)) 65 | { 66 | //logf("found it"); 67 | return cur.def.frag.ss.sel; 68 | } else { 69 | //logf("not found"); 70 | } 71 | } 72 | cur = cur.follow; 73 | } 74 | 75 | //logf("search failed"); 76 | return null; 77 | } 78 | 79 | unittest { 80 | string s = `{ 81 | user(id: 1) { 82 | friends { 83 | name 84 | } 85 | name 86 | age 87 | } 88 | }`; 89 | auto l = Lexer(s); 90 | auto p = Parser(l); 91 | auto d = p.parseDocument(); 92 | 93 | const f = findFragment(d, "fooo", ["user"]); 94 | assert(f is null); 95 | } 96 | 97 | unittest { 98 | string s = ` 99 | fragment fooo on Hero { 100 | name 101 | }`; 102 | auto l = Lexer(s); 103 | auto p = Parser(l); 104 | auto d = p.parseDocument(); 105 | 106 | auto f = findFragment(d, "fooo", ["Hero"]); 107 | assert(f !is null); 108 | assert(f.sel.field.name.name.tok.value == "name"); 109 | 110 | f = findFragment(d, "fooo", ["Villian"]); 111 | assert(f is null); 112 | } 113 | 114 | alias BuildLinear = Flag!"BuildLinear"; 115 | 116 | struct FieldRangeItem { 117 | import std.array : empty; 118 | Field f; 119 | Document doc; 120 | 121 | @property string name() { 122 | return f.name.name.tok.value; 123 | //return f.name.aka.value.empty ? f.name.name.tok.value : f.name.aka.value; 124 | } 125 | 126 | @property string aka() { 127 | return f.name.aka.value; 128 | } 129 | } 130 | 131 | struct FieldRange { 132 | Selections[] cur; 133 | Document doc; 134 | string[] typenames; 135 | Selection[] linear; 136 | Json vars; 137 | 138 | this(Selections sels, Document doc, string[] typenames, Json vars 139 | , BuildLinear buildLinear) 140 | { 141 | this.doc = doc; 142 | this.typenames = typenames; 143 | this.vars = vars; 144 | this.cur ~= sels; 145 | if(buildLinear == BuildLinear.yes) { 146 | this.linear = copySelection(sels); 147 | } 148 | this.build(); 149 | //this.test(); 150 | } 151 | 152 | @property bool empty() const pure { 153 | return this.cur.length == 0; 154 | } 155 | 156 | @property FieldRangeItem front() { 157 | enforce(!this.cur.empty); 158 | enforce(this.cur.back !is null); 159 | enforce(this.cur.back.sel !is null); 160 | enforce(this.cur.back.sel.field !is null); 161 | return FieldRangeItem(this.cur.back.sel.field, this.doc); 162 | } 163 | 164 | bool directivesAllowContinue(Selection sel, Json vars) { 165 | Directives dirs; 166 | final switch(sel.ruleSelection) { 167 | case SelectionEnum.Field: 168 | dirs = sel.field.dirs; 169 | break; 170 | case SelectionEnum.Spread: 171 | dirs = sel.frag.dirs; 172 | break; 173 | case SelectionEnum.IFrag: 174 | dirs = sel.ifrag.dirs; 175 | break; 176 | } 177 | return continueAfterDirectives(dirs, vars); 178 | } 179 | 180 | void popFront() { 181 | this.cur.back = this.cur.back.follow; 182 | this.build(); 183 | } 184 | 185 | void build() { 186 | if(this.cur.empty) { 187 | return; 188 | } 189 | if(this.cur.back !is null 190 | && directivesAllowContinue(this.cur.back.sel, vars)) 191 | { 192 | const SelectionEnum se = this.cur.back.sel.ruleSelection; 193 | if(se == SelectionEnum.Field) { 194 | return; 195 | } else { 196 | Selections f = se == SelectionEnum.Spread 197 | ? findFragment(doc, this.cur.back.sel.frag.name.value, 198 | this.typenames 199 | ) 200 | : resolveInlineFragment(this.cur.back.sel.ifrag, 201 | this.typenames 202 | ); 203 | 204 | Selections follow = this.cur.back.follow; 205 | this.cur = this.cur[0 .. $ - 1]; 206 | 207 | if(follow !is null) { 208 | this.cur ~= follow; 209 | this.build(); 210 | //this.test(); 211 | } 212 | if(f !is null) { 213 | this.cur ~= f; 214 | this.build(); 215 | //this.test(); 216 | } 217 | } 218 | } else if(this.cur.back is null) { 219 | this.cur = this.cur[0 .. $ - 1]; 220 | this.build(); 221 | } else { 222 | this.cur.back = this.cur.back.follow; 223 | this.build(); 224 | } 225 | } 226 | 227 | } 228 | 229 | Selections resolveInlineFragment(InlineFragment ilf, string[] typenames) { 230 | import std.algorithm.searching : canFind; 231 | final switch(ilf.ruleSelection) { 232 | case InlineFragmentEnum.TDS: 233 | goto case InlineFragmentEnum.TS; 234 | case InlineFragmentEnum.TS: 235 | return canFind(typenames, ilf.tc.value) ? ilf.ss.sel : null; 236 | case InlineFragmentEnum.DS: 237 | return ilf.ss.sel; 238 | case InlineFragmentEnum.S: 239 | return ilf.ss.sel; 240 | } 241 | } 242 | 243 | FieldRange fieldRange(OperationDefinition od, Document doc, 244 | string[] typenames, BuildLinear buildLinear = BuildLinear.no) 245 | { 246 | return FieldRange(od.accessNN!(["ss", "sel"]), doc, typenames, 247 | Json.emptyObject(), buildLinear); 248 | } 249 | 250 | FieldRange fieldRange(SelectionSet ss, Document doc, string[] typenames 251 | , BuildLinear buildLinear = BuildLinear.no) 252 | { 253 | return FieldRange(ss.sel, doc, typenames, Json.emptyObject() 254 | , buildLinear); 255 | } 256 | 257 | FieldRange fieldRange(SelectionSet ss, Document doc, string[] typenames 258 | , Json vars 259 | , BuildLinear buildLinear = BuildLinear.no) 260 | { 261 | return FieldRange(ss.sel, doc, typenames, vars, buildLinear); 262 | } 263 | 264 | FieldRangeItem[] fieldRangeArr(Selections sel, Document doc 265 | , string[] typenames, Json vars 266 | , BuildLinear buildLinear = BuildLinear.no) 267 | { 268 | import std.array : array; 269 | return FieldRange(sel, doc, typenames, vars, buildLinear).array; 270 | } 271 | 272 | FieldRangeItem[] fieldRangeArr(Selections sel, Document doc, 273 | string[] typenames, BuildLinear buildLinear = BuildLinear.no) 274 | { 275 | import std.array : array; 276 | return fieldRangeArr(sel, doc, typenames, Json.emptyObject(), BuildLinear.no); 277 | } 278 | 279 | private Selection[] copySelection(Selections s) { 280 | Selection[] ret; 281 | Selections cur = s; 282 | while(cur !is null) { 283 | ret ~= cur.sel; 284 | cur = cur.follow; 285 | } 286 | return ret; 287 | } 288 | 289 | struct OpDefRangeItem { 290 | Document doc; 291 | Definition def; 292 | 293 | FieldRange fieldRange(string[] typenames) { 294 | return .fieldRange(accessNN!(["op", "ss"])(this.def), this.doc, 295 | typenames 296 | ); 297 | } 298 | } 299 | 300 | struct OpDefRange { 301 | Document doc; 302 | Definitions defs; 303 | 304 | this(Document doc) { 305 | this.doc = doc; 306 | this.defs = doc.defs; 307 | this.advance(); 308 | } 309 | 310 | private void advance() { 311 | while(this.defs !is null 312 | && this.defs.def.ruleSelection != DefinitionEnum.O) 313 | { 314 | this.defs = this.defs.follow; 315 | } 316 | } 317 | 318 | @property bool empty() const { 319 | return this.defs is null; 320 | } 321 | 322 | @property OpDefRangeItem front() { 323 | return OpDefRangeItem(this.doc, this.defs.def); 324 | } 325 | 326 | void popFront() { 327 | this.defs = this.defs.follow; 328 | this.advance(); 329 | } 330 | 331 | @property typeof(this) save() { 332 | OpDefRange ret; 333 | ret.doc = this.doc; 334 | ret.defs = this.defs; 335 | return ret; 336 | } 337 | } 338 | 339 | OpDefRange opDefRange(Document doc) { 340 | return OpDefRange(doc); 341 | } 342 | 343 | unittest { 344 | string s = `{ 345 | user(id: 1) { 346 | friends { 347 | name 348 | } 349 | name 350 | age 351 | } 352 | }`; 353 | auto l = Lexer(s); 354 | auto p = Parser(l); 355 | auto d = p.parseDocument(); 356 | 357 | FieldRange r = fieldRange(d.defs.def.op, d, ["User"]); 358 | assert(!r.empty); 359 | assert(r.front.name == "user"); 360 | } 361 | 362 | unittest { 363 | string s = `{ 364 | user(id: 1) { 365 | ...foo 366 | } 367 | } 368 | 369 | fragment foo on User { 370 | name 371 | age 372 | } 373 | `; 374 | auto l = Lexer(s); 375 | auto p = Parser(l); 376 | auto d = p.parseDocument(); 377 | 378 | const f = findFragment(d, "foo", ["User"]); 379 | assert(f !is null); 380 | 381 | FieldRange r = fieldRange(d.defs.def.op, d, ["User"]); 382 | assert(!r.empty); 383 | assert(r.front.name == "user"); 384 | } 385 | 386 | unittest { 387 | string s = `{ 388 | user(id: 1) { 389 | ...foo 390 | ...bar 391 | } 392 | } 393 | 394 | fragment foo on User { 395 | name 396 | } 397 | 398 | fragment bar on User { 399 | age 400 | } 401 | `; 402 | auto l = Lexer(s); 403 | auto p = Parser(l); 404 | auto d = p.parseDocument(); 405 | 406 | const f = findFragment(d, "foo", ["User"]); 407 | assert(f !is null); 408 | 409 | const f2 = findFragment(d, "bar", ["User"]); 410 | assert(f2 !is null); 411 | 412 | FieldRange r = fieldRange(d.defs.def.op, d, ["User"]); 413 | assert(!r.empty); 414 | assert(r.front.name == "user"); 415 | } 416 | 417 | unittest { 418 | string s = `{ 419 | user(id: 1) { 420 | ...foo 421 | ...bar 422 | hello 423 | } 424 | } 425 | 426 | fragment foo on User { 427 | name 428 | } 429 | 430 | fragment bar on User { 431 | age 432 | } 433 | `; 434 | auto l = Lexer(s); 435 | auto p = Parser(l); 436 | auto d = p.parseDocument(); 437 | 438 | const f = findFragment(d, "foo", ["User"]); 439 | assert(f !is null); 440 | 441 | const f2 = findFragment(d, "bar", ["User"]); 442 | assert(f2 !is null); 443 | 444 | FieldRange r = fieldRange(d.defs.def.op, d, ["User"]); 445 | assert(!r.empty); 446 | assert(r.front.name == "user"); 447 | } 448 | 449 | unittest { 450 | string s = `{ 451 | user(id: 1) { 452 | hello 453 | ...foo 454 | ...bar 455 | } 456 | } 457 | 458 | fragment foo on User { 459 | name 460 | } 461 | 462 | fragment bar on User { 463 | age 464 | } 465 | `; 466 | auto l = Lexer(s); 467 | auto p = Parser(l); 468 | auto d = p.parseDocument(); 469 | 470 | const f = findFragment(d, "foo", ["User"]); 471 | assert(f !is null); 472 | 473 | const f2 = findFragment(d, "bar", ["User"]); 474 | assert(f2 !is null); 475 | 476 | FieldRange r = fieldRange(d.defs.def.op, d, ["User"]); 477 | assert(!r.empty); 478 | assert(r.front.name == "user"); 479 | } 480 | 481 | unittest { 482 | string s = `{ 483 | user(id: 1) { 484 | hello 485 | ...foo 486 | ...bar 487 | } 488 | } 489 | 490 | fragment foo on User { 491 | name 492 | } 493 | 494 | fragment bar on User { 495 | age 496 | ...baz 497 | } 498 | 499 | fragment baz on User { 500 | args 501 | } 502 | `; 503 | auto l = Lexer(s); 504 | auto p = Parser(l); 505 | auto d = p.parseDocument(); 506 | 507 | const f = findFragment(d, "foo", ["User"]); 508 | assert(f !is null); 509 | 510 | const f2 = findFragment(d, "bar", ["User"]); 511 | assert(f2 !is null); 512 | 513 | FieldRange r = fieldRange(d.defs.def.op, d, ["User"]); 514 | assert(!r.empty); 515 | assert(r.front.name == "user"); 516 | } 517 | 518 | unittest { 519 | string s = `{ 520 | user(id: 1) { 521 | hello 522 | ...foo 523 | zzzz 524 | ...bar 525 | } 526 | } 527 | 528 | fragment foo on User { 529 | name 530 | } 531 | 532 | fragment bar on User { 533 | age 534 | ...baz 535 | } 536 | 537 | fragment baz on User { 538 | args 539 | } 540 | `; 541 | auto l = Lexer(s); 542 | auto p = Parser(l); 543 | auto d = p.parseDocument(); 544 | 545 | const f = findFragment(d, "foo", ["User"]); 546 | assert(f !is null); 547 | 548 | const f2 = findFragment(d, "bar", ["User"]); 549 | assert(f2 !is null); 550 | 551 | FieldRange r = fieldRange(d.defs.def.op, d, ["User"]); 552 | assert(!r.empty); 553 | assert(r.front.name == "user"); 554 | } 555 | 556 | unittest { 557 | import std.format : format; 558 | import std.stdio; 559 | 560 | string s = `{ 561 | user(id: 1) { 562 | hello 563 | ...foo 564 | zzzz 565 | ...bar 566 | } 567 | } 568 | 569 | fragment foo on User { 570 | name 571 | } 572 | 573 | fragment bar on User { 574 | age 575 | ...baz 576 | } 577 | 578 | fragment baz on User { 579 | args 580 | } 581 | `; 582 | auto l = Lexer(s); 583 | auto p = Parser(l); 584 | auto d = p.parseDocument(); 585 | 586 | immutable auto nn = ["hello", "name", "zzzz", "age", "args"]; 587 | size_t cnt = 0; 588 | foreach(it; opDefRange(d)) { 589 | ++cnt; 590 | foreach(jt; it.fieldRange(["User"])) { 591 | } 592 | } 593 | assert(cnt == 1); 594 | } 595 | -------------------------------------------------------------------------------- /source/graphql/client/document.d: -------------------------------------------------------------------------------- 1 | /// A CTFE-friendly representation of (a subset of) 2 | /// GraphQL documents (schemas and queries). 3 | module graphql.client.document; 4 | 5 | // This is an internal module. 6 | package(graphql): 7 | 8 | import ast = graphql.ast; 9 | 10 | /* 11 | Q: Why mirror the AST type hierarchy, isn't it redundant? 12 | A: 1. By restricting the representation to a certain subset of the D type system, 13 | we can allow the representation to be used in more ways (e.g. as "enum" 14 | instead of "static const"). 15 | 2. Dealing with the AST is a little verbose in some situations (e.g. 16 | VariableDefinition.var.name.value), doing this as a separate step 17 | allows us to avoid pushing this complexity into the code generating D types. 18 | 3. We can cull parts of the document that are not relevant to D code generation. 19 | 4. By walking the AST ahead of time, we can do some work only once instead of 20 | per query template instantiation (`schema.query!"..."`). 21 | 5. In case parsing the schema at compile-time becomes too expensive, 22 | in the future we could add support to dumping the parsed representation 23 | of the GraphQL schema to .d files (which would be useful as the GraphQL 24 | schema usually changes much rarer than the queries when building clients). 25 | */ 26 | 27 | struct Type { 28 | // Only one (of name / list / nullable) may be set: 29 | string name; 30 | // We use a 0-or-1-element dynamic array instead of a pointer, 31 | // because struct pointers are not very CTFE-friendly 32 | Type[] list; 33 | Type[] nullable; 34 | 35 | this(ast.Type t) { 36 | final switch (t.ruleSelection) { 37 | case ast.TypeEnum.TN: 38 | name = t.tname.value; 39 | break; 40 | case ast.TypeEnum.LN: 41 | list = [Type(t.list.type)]; 42 | break; 43 | case ast.TypeEnum.T: 44 | nullable.length = 1; 45 | nullable[0].name = t.tname.value; 46 | break; 47 | case ast.TypeEnum.L: 48 | nullable.length = 1; 49 | nullable[0].list = [Type(t.list.type)]; 50 | break; 51 | } 52 | } 53 | } 54 | 55 | struct FieldDefinition { 56 | string name; 57 | Type type; 58 | 59 | this(ast.FieldDefinition fd) { 60 | if (auto des = fd.des) { /* TODO handle description */ } 61 | if (auto arg = fd.arg) { /* TODO handle arguments */ } 62 | if (auto dir = fd.dir) { /* TODO handle directives */ } 63 | 64 | this.name = fd.name.tok.value; 65 | this.type = Type(fd.typ); 66 | } 67 | } 68 | 69 | struct InterfaceTypeDefinition { 70 | string name; 71 | FieldDefinition[] fields; 72 | 73 | this(ast.InterfaceTypeDefinition itd) { 74 | this.name = itd.name.value; 75 | for (auto fds = itd.fds; fds !is null; fds = fds.follow) { 76 | if (auto fd = fds.fd) { 77 | this.fields ~= FieldDefinition(fd); 78 | } 79 | } 80 | } 81 | } 82 | 83 | struct ObjectTypeDefinition { 84 | string name; 85 | string[] implementsInterfaces; 86 | FieldDefinition[] fields; 87 | 88 | this(ast.ObjectTypeDefinition otd) { 89 | this.name = otd.name.value; 90 | for (auto fds = otd.fds; fds !is null; fds = fds.follow) { 91 | if (auto fd = fds.fd) { 92 | this.fields ~= FieldDefinition(fd); 93 | } 94 | } 95 | if (auto ii = otd.ii) { 96 | for (auto nts = ii.nts; nts !is null; nts = nts.follow) { 97 | this.implementsInterfaces ~= nts.name.value; 98 | } 99 | } 100 | } 101 | } 102 | 103 | struct InputValueDefinition { 104 | string name; 105 | Type type; 106 | 107 | this(ast.InputValueDefinition ivd) { 108 | this.name = ivd.name.tok.value; 109 | this.type = Type(ivd.type); 110 | } 111 | } 112 | 113 | struct InputObjectTypeDefinition { 114 | string name; 115 | InputValueDefinition[] values; 116 | 117 | this(ast.InputObjectTypeDefinition otd) { 118 | this.name = otd.name.value; 119 | for (auto ivds = otd.ivds; ivds !is null; ivds = ivds.follow) { 120 | if (auto iv = ivds.iv) { 121 | this.values ~= InputValueDefinition(iv); 122 | } 123 | } 124 | } 125 | } 126 | 127 | struct ScalarTypeDefinition { 128 | string name; 129 | 130 | this(ast.ScalarTypeDefinition std) { 131 | this.name = std.name.value; 132 | } 133 | } 134 | 135 | struct EnumValueDefinition { 136 | string name; 137 | 138 | this(ast.EnumValueDefinition evd) { 139 | this.name = evd.name.value; 140 | } 141 | } 142 | 143 | struct EnumTypeDefinition { 144 | string name; 145 | EnumValueDefinition[] values; 146 | 147 | this(ast.EnumTypeDefinition etd) { 148 | this.name = etd.name.value; 149 | for (auto evds = etd.evds; evds !is null; evds = evds.follow) { 150 | if (auto evd = evds.evd) { 151 | this.values ~= EnumValueDefinition(evd); 152 | } 153 | } 154 | } 155 | } 156 | 157 | alias OperationType = ast.OperationTypeEnum; 158 | 159 | struct OperationTypeDefinition { 160 | OperationType type; 161 | string name; 162 | 163 | this(ast.OperationTypeDefinition otd) { 164 | this.type = otd.ot.ruleSelection; 165 | this.name = otd.nt.value; 166 | } 167 | 168 | this(OperationType type, string name) { 169 | this.type = type; 170 | this.name = name; 171 | } 172 | } 173 | 174 | struct SchemaDefinition { 175 | OperationTypeDefinition[] operationTypes; 176 | 177 | this(ast.SchemaDefinition sch) { 178 | for (auto otds = sch.otds; otds !is null; otds = otds.follow) { 179 | if (otds.otd) { 180 | this.operationTypes ~= OperationTypeDefinition(otds.otd); 181 | } 182 | } 183 | } 184 | 185 | this(OperationTypeDefinition[] operationTypes) { 186 | this.operationTypes = operationTypes; 187 | } 188 | } 189 | 190 | struct SchemaDocument { 191 | SchemaDefinition schema; 192 | ObjectTypeDefinition[] objectTypes; 193 | InterfaceTypeDefinition[] interfaceTypes; 194 | ScalarTypeDefinition[] scalarTypes; 195 | EnumTypeDefinition[] enumTypes; 196 | InputObjectTypeDefinition[] inputTypes; 197 | 198 | this(ast.Document d) { 199 | for (auto defs = d.defs; defs !is null; defs = defs.follow) { 200 | if (auto def = defs.def) { 201 | if (auto type = def.type) { 202 | if (auto sch = type.sch) { 203 | assert(schema is SchemaDefinition.init, 204 | "Multiple schema definitions in document"); 205 | this.schema = SchemaDefinition(sch); 206 | } 207 | 208 | if (auto td = type.td) { 209 | if (auto otd = td.otd) { 210 | this.objectTypes ~= ObjectTypeDefinition(otd); 211 | } 212 | if (auto itd = td.itd) { 213 | this.interfaceTypes ~= InterfaceTypeDefinition(itd); 214 | } 215 | if (auto std = td.std) { 216 | this.scalarTypes ~= ScalarTypeDefinition(std); 217 | } 218 | if (auto etd = td.etd) { 219 | this.enumTypes ~= EnumTypeDefinition(etd); 220 | } 221 | if (auto iod = td.iod) { 222 | this.inputTypes ~= InputObjectTypeDefinition(iod); 223 | } 224 | } 225 | } 226 | } 227 | } 228 | 229 | if (this.schema is SchemaDefinition.init) { 230 | // Populate with the default 231 | this.schema = SchemaDefinition([ 232 | OperationTypeDefinition(OperationType.Query, "Query"), 233 | OperationTypeDefinition(OperationType.Mutation, "Mutation"), 234 | OperationTypeDefinition(OperationType.Sub, "Subscription"), 235 | ]); 236 | } 237 | } 238 | } 239 | 240 | struct Field { 241 | string name; 242 | Field[] selections; 243 | 244 | this(ast.Field f) { 245 | this.name = f.name.name.tok.value; 246 | if (auto ss = f.ss) { 247 | for (auto sels = ss.sel; sels !is null; sels = sels.follow) { 248 | if (auto sel = sels.sel) { 249 | if (auto field = sel.field) { 250 | this.selections ~= Field(field); 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } 257 | 258 | struct VariableDefinition { 259 | string name; 260 | Type type; 261 | 262 | this(ast.VariableDefinition vd) { 263 | this.name = vd.var.name.value; 264 | this.type = Type(vd.type); 265 | } 266 | } 267 | 268 | struct OperationDefinition { 269 | OperationType type; 270 | string name; 271 | VariableDefinition[] variables; 272 | Field[] selections; 273 | 274 | this(ast.OperationDefinition od) { 275 | this.type = od.ot ? od.ot.ruleSelection : OperationType.Query; 276 | this.name = od.name.value; 277 | if (auto vd = od.vd) { 278 | for (auto vars = vd.vars; vars !is null; vars = vars.follow) { 279 | if (auto var = vars.var) { 280 | this.variables ~= VariableDefinition(var); 281 | } 282 | } 283 | } 284 | if (auto ss = od.ss) { 285 | for (auto sels = ss.sel; sels !is null; sels = sels.follow) { 286 | if (auto sel = sels.sel) { 287 | if (auto field = sel.field) { 288 | this.selections ~= Field(field); 289 | } 290 | } 291 | } 292 | } 293 | } 294 | } 295 | 296 | struct QueryDocument { 297 | this(ast.Document d) { 298 | for (auto defs = d.defs; defs !is null; defs = defs.follow) { 299 | if (auto def = defs.def) { 300 | if (auto op = def.op) { 301 | operations ~= OperationDefinition(op); 302 | } 303 | } 304 | } 305 | } 306 | 307 | OperationDefinition[] operations; 308 | } 309 | -------------------------------------------------------------------------------- /source/graphql/client/query.d: -------------------------------------------------------------------------------- 1 | /// Building blocks for type-safe GraphQL queries. 2 | module graphql.client.query; 3 | 4 | import graphql.client.document; 5 | import graphql.client.codegen : toD, CodeGenerationSettings; 6 | import graphql.lexer; 7 | import graphql.parser; 8 | 9 | public import graphql.client.codegen : SerializationLibraries; 10 | 11 | struct GraphQLSettings { 12 | SerializationLibraries serializationLibraries; 13 | } 14 | 15 | /// Represents a parsed GraphQL schema, an object which can serve 16 | /// as a base for building GraphQL queries. 17 | struct GraphQLSchema(alias document_, GraphQLSettings settings_) { 18 | alias document = document_; 19 | alias settings = settings_; 20 | 21 | enum code = { 22 | CodeGenerationSettings settings; 23 | settings.serializationLibraries = this.settings.serializationLibraries; 24 | settings.schemaRefExpr = q{}; 25 | return toD(document, settings); 26 | }(); 27 | 28 | mixin(code); 29 | 30 | template query(string queryText_) { 31 | static immutable queryText = queryText_; 32 | enum query = GraphQLQuery!(typeof(this), queryText).init; 33 | } 34 | } 35 | 36 | struct GraphQLQuery(GraphQLSchema, alias queryText_) { 37 | alias queryText = queryText_; 38 | static const document = { 39 | auto l = Lexer(queryText, QueryParser.yes); 40 | auto p = Parser(l); 41 | auto d = p.parseDocument(); 42 | 43 | return QueryDocument(d); 44 | }(); 45 | 46 | mixin({ 47 | CodeGenerationSettings settings; 48 | settings.serializationLibraries = GraphQLSchema.settings.serializationLibraries; 49 | settings.schemaRefExpr = q{GraphQLSchema.}; 50 | return toD(document, GraphQLSchema.document, settings); 51 | }()); 52 | } 53 | 54 | enum isQueryInstance(T) = 55 | is(T.Query == GraphQLQuery!(GraphQLSchema, queryText), GraphQLSchema, string queryText); 56 | 57 | /// Parses a GraphQL schema at compile-time, returning a 58 | /// compile-time-accessible representation of the schema. 59 | auto graphqlSchema( 60 | alias/*string*/ schemaText, 61 | GraphQLSettings settings = GraphQLSettings() 62 | )() { 63 | static const document = { 64 | auto l = Lexer(schemaText, QueryParser.no); 65 | auto p = Parser(l); 66 | auto d = p.parseDocument(); 67 | 68 | return SchemaDocument(d); 69 | }(); 70 | return GraphQLSchema!(document, settings)(); 71 | } 72 | 73 | // Basic ReturnType test 74 | unittest { 75 | static immutable schema = graphqlSchema!` 76 | type Query { 77 | hello: String! 78 | } 79 | `; 80 | 81 | immutable query = schema.query!` 82 | query { 83 | hello 84 | } 85 | `; 86 | 87 | static assert(is(typeof(query.ReturnType.hello) == string)); 88 | } 89 | 90 | // Variables test 91 | unittest { 92 | static immutable schema = graphqlSchema!` 93 | type User { 94 | id: ID! 95 | name: String! 96 | age: Int! 97 | } 98 | 99 | type Mutation { 100 | updateUser(id: ID!, name: String!): User! 101 | } 102 | `; 103 | 104 | immutable updateUser = schema.query!` 105 | mutation($id: ID!, $name: String!) { 106 | updateUser(id: $id, name: $name) { 107 | id 108 | name 109 | } 110 | } 111 | `; 112 | 113 | // We want to be able to write something like: 114 | // 115 | // auto client = new VibeHttpGraphQLClient("http://.../graphql"); 116 | // client.call(updateUser(id: "1", name: "John Doe")) 117 | // .updateUser.name 118 | // .writefln!"Name changed to %s"; 119 | 120 | auto op = updateUser(id: "1", name: "John Doe"); 121 | static assert(is(typeof(op.variables.name) == string)); 122 | static assert(is(typeof(op.Query.ReturnType.updateUser.name) == string)); 123 | static assert(!is(typeof(op.Query.ReturnType.updateUser.age))); 124 | } 125 | 126 | 127 | // Test arrays and (non-)nullables 128 | unittest { 129 | import std.typecons : Nullable; 130 | 131 | static immutable schema = graphqlSchema!` 132 | type Foo { 133 | i: Int! 134 | } 135 | 136 | type Query { 137 | v: Int! 138 | vn: Int 139 | va: [Int!]! 140 | vna: [Int]! 141 | van: [Int!] 142 | vnan: [Int] 143 | 144 | s: Foo! 145 | sn: Foo 146 | sa: [Foo!]! 147 | sna: [Foo]! 148 | san: [Foo!] 149 | snan: [Foo] 150 | } 151 | `; 152 | 153 | immutable query = schema.query!` 154 | query { 155 | v 156 | vn 157 | va 158 | vna 159 | van 160 | vnan 161 | 162 | s { i } 163 | sn { i } 164 | sa { i } 165 | sna { i } 166 | san { i } 167 | snan { i } 168 | } 169 | `; 170 | 171 | static assert(is(typeof(query.ReturnType.v) == int)); 172 | static assert(is(typeof(query.ReturnType.vn.get()) == int)); 173 | static assert(is(typeof(query.ReturnType.va[0]) == int)); 174 | static assert(is(typeof(query.ReturnType.vna[0].get()) == int)); 175 | static assert(is(typeof(query.ReturnType.van.get()[0]) == int)); 176 | static assert(is(typeof(query.ReturnType.vnan.get()[0].get()) == int)); 177 | 178 | static assert(is(typeof(query.ReturnType.s.i) == int)); 179 | static assert(is(typeof(query.ReturnType.sn.get().i) == int)); 180 | static assert(is(typeof(query.ReturnType.sa[0].i) == int)); 181 | static assert(is(typeof(query.ReturnType.sna[0].get().i) == int)); 182 | static assert(is(typeof(query.ReturnType.san.get()[0].i) == int)); 183 | static assert(is(typeof(query.ReturnType.snan.get()[0].get().i) == int)); 184 | } 185 | 186 | // Test implicit operation type 187 | unittest { 188 | static immutable schema = graphqlSchema!` 189 | type Query { 190 | i: Int! 191 | } 192 | `; 193 | 194 | immutable query = schema.query!`{ i }`; 195 | 196 | static assert(is(typeof(query.ReturnType.i) == int)); 197 | static assert(isQueryInstance!(typeof(query()))); 198 | } 199 | 200 | // Test __typename 201 | unittest { 202 | static immutable schema = graphqlSchema!` 203 | type S { 204 | i: Int 205 | } 206 | 207 | type Query { 208 | s: S! 209 | } 210 | `; 211 | 212 | immutable query = schema.query!`{ 213 | __typename 214 | s { 215 | __typename 216 | } 217 | }`; 218 | 219 | static assert(is(typeof(query.ReturnType.__typename) == string)); 220 | static assert(is(typeof(query.ReturnType.s.__typename) == string)); 221 | } 222 | 223 | // Test schema type generation 224 | unittest { 225 | static immutable schema = graphqlSchema!` 226 | type S { 227 | str: String! 228 | next: S 229 | } 230 | `; 231 | 232 | static assert(is(typeof(schema.Schema.S.init.str) == string)); 233 | static assert(is(typeof(schema.Schema.S.init.next) == schema.Schema.S)); 234 | } 235 | 236 | // Test schema type construction 237 | unittest { 238 | import std.typecons : nullable; 239 | 240 | static immutable schema = graphqlSchema!` 241 | type S { 242 | str: String! 243 | next: S 244 | } 245 | `; 246 | 247 | auto s = new schema.Schema.S( 248 | str: "first", 249 | next: new schema.Schema.S( 250 | str: "second" 251 | ) 252 | ); 253 | } 254 | 255 | // Test types with interfaces 256 | unittest { 257 | static immutable schema = graphqlSchema!` 258 | interface Node { 259 | nodeId: ID! 260 | } 261 | type SomeNode implements Node { 262 | nodeId: ID! 263 | } 264 | type Query { 265 | someNode: SomeNode! 266 | } 267 | `; 268 | 269 | static assert(is(typeof(schema.Schema.SomeNode.init.nodeId) == string)); 270 | 271 | immutable query = schema.query!`{ 272 | someNode { 273 | nodeId 274 | } 275 | }`; 276 | 277 | static assert(is(typeof(query.ReturnType.someNode.nodeId) == string)); 278 | } 279 | -------------------------------------------------------------------------------- /source/graphql/client/vibe.d: -------------------------------------------------------------------------------- 1 | /// Vibe.d HTTP GraphQL client 2 | module graphql.client.vibe; 3 | 4 | import vibe.data.json; 5 | import vibe.http.client; 6 | import vibe.stream.operations : readAllUTF8; 7 | 8 | import graphql.client.query : isQueryInstance; 9 | 10 | @safe: 11 | 12 | final class VibeHttpGraphQLClient { 13 | string url; 14 | string[string] headers; 15 | 16 | this(string url, string[string] headers = null) { 17 | this.url = url; 18 | this.headers = headers; 19 | } 20 | 21 | private Json callImpl(string query, Json variables) { 22 | Json body = Json.emptyObject; 23 | body["query"] = query; 24 | body["variables"] = variables; 25 | 26 | Json result; 27 | requestHTTP(url, 28 | (scope req) { 29 | req.method = HTTPMethod.POST; 30 | req.headers["Content-Type"] = "application/json"; 31 | foreach (key, value; headers) { 32 | req.headers[key] = value; 33 | } 34 | 35 | req.writeJsonBody(body); 36 | }, 37 | (scope res) { 38 | result = res.bodyReader.readAllUTF8().parseJsonString(); 39 | } 40 | ); 41 | return result; 42 | } 43 | 44 | struct Error { 45 | string message; 46 | struct Location { 47 | int line; 48 | int column; 49 | } 50 | @optional Location[] locations; 51 | @optional Json[] path; 52 | @optional Json[string] extensions; 53 | } 54 | 55 | static class GraphQLException : Exception { 56 | Error[] errors; 57 | this(Error[] errors) in(errors.length > 0) { 58 | this.errors = errors; 59 | super(errors[0].message); 60 | } 61 | } 62 | 63 | QueryInstance.Query.ReturnType call(QueryInstance)(QueryInstance queryInstance) 64 | if (isQueryInstance!QueryInstance) { 65 | struct Response { 66 | @optional QueryInstance.Query.ReturnType data; 67 | @optional Error[] errors; 68 | } 69 | 70 | auto response = callImpl( 71 | queryInstance.Query.queryText, 72 | queryInstance.variables.serializeToJson() 73 | ).deserializeJson!Response; 74 | 75 | if (response.errors.length > 0) { 76 | throw new GraphQLException(response.errors); 77 | } 78 | 79 | return response.data; 80 | } 81 | } 82 | 83 | unittest { 84 | if (false) { // Test instantiation only 85 | import graphql.client.query : graphqlSchema; 86 | 87 | static immutable schema = graphqlSchema!` 88 | type User { 89 | id: ID! 90 | name: String! 91 | } 92 | 93 | type Mutation { 94 | updateUser(id: ID!, name: String!): User! 95 | } 96 | `; 97 | 98 | immutable updateUser = schema.query!` 99 | mutation($id: ID!, $name: String!) { 100 | updateUser(id: $id, name: $name) { 101 | id 102 | name 103 | } 104 | } 105 | `; 106 | 107 | auto client = new VibeHttpGraphQLClient("http://.../graphql"); 108 | auto newName = client.call(updateUser(id: "1", name: "John Doe")) 109 | .updateUser.name; 110 | static assert(is(typeof(newName) == string)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /source/graphql/constants.d: -------------------------------------------------------------------------------- 1 | module graphql.constants; 2 | 3 | enum Constants { 4 | ENUM = "ENUM", 5 | ENUM_DEPRECATIONREASON_TODO = "ENUM_DEPRECATIONREASON_TODO", 6 | ENUM_DESCRIPTION_TODO = "ENUM_DESCRIPTION_TODO", 7 | Float = "Float", 8 | INPUT_OBJECT = "INPUT_OBJECT", 9 | Int = "Int", 10 | LIST = "LIST", 11 | List = "List", 12 | Mutation = "Mutation", 13 | NON_NULL = "NON_NULL", 14 | NonNull = "NonNull", 15 | Nullable = "Nullable", 16 | OBJECT = "OBJECT", 17 | Operation = "Operation", 18 | Query = "Query", 19 | Schema = "Schema", 20 | String = "String", 21 | Subscription = "Subscription", 22 | UNION = "UNION", 23 | __Directive = "__Directive", 24 | __DirectiveLocation = "__DirectiveLocation", 25 | __EnumValue = "__EnumValue", 26 | __Field = "__Field", 27 | __InputValue = "__InputValue", 28 | __Type = "__Type", 29 | __TypeKind = "__TypeKind", 30 | __listType = "__listType", 31 | __nonNullType = "__nonNullType", 32 | __schema = "__schema", 33 | __type = "__type", 34 | __typename = "__typename", 35 | args = "args", 36 | data = "data", 37 | defaultValue = "defaultValue", 38 | deprecationReason = "deprecationReason", 39 | description = "description", 40 | directives = "directives", 41 | enumValues = "enumValues", 42 | fields = "fields", 43 | inputFields = "inputFields", 44 | interfaces = "interfaces", 45 | interfacesNames = "interfacesNames", 46 | isDeprecated = "isDeprecated", 47 | kind = "kind", 48 | errors = "errors", 49 | locations = "locations", 50 | mutation = "mutation", 51 | mutationType = "mutationType", 52 | name = "name", 53 | null_ = "null", 54 | ofType = "ofType", 55 | ofTypeName = "ofTypeName", 56 | opCmp = "opCmp", 57 | opEquals = "opEquals", 58 | possibleTypes = "possibleTypes", 59 | possibleTypesNames = "possibleTypesNames", 60 | query = "query", 61 | queryType = "queryType", 62 | string_ = "string", 63 | subscription = "subscription", 64 | subscriptionType = "subscriptionType", 65 | toHash = "toHash", 66 | toString = "toString", 67 | true_ = "true", 68 | type = "type", 69 | typenameOrig = "typenameOrig", 70 | types = "types", 71 | } 72 | -------------------------------------------------------------------------------- /source/graphql/directives.d: -------------------------------------------------------------------------------- 1 | module graphql.directives; 2 | 3 | import std.format : format; 4 | 5 | import vibe.data.json; 6 | 7 | import graphql.ast; 8 | import graphql.helper; 9 | import graphql.astselector; 10 | 11 | @safe: 12 | 13 | private enum Include { 14 | undefined = 0, 15 | yes = 1, 16 | no = 4 17 | } 18 | 19 | private enum Skip { 20 | undefined = 0, 21 | yes = 1, 22 | no = 4 23 | } 24 | 25 | T add(T)(T a, T b) { 26 | immutable int ai = a; 27 | immutable int bi = b; 28 | int r = ai + bi; 29 | if(r == 0) { 30 | return cast(T)r; 31 | } else if(r == 1 || r == 2) { 32 | return T.yes; 33 | } else if(r == 4 || r == 8) { 34 | return T.no; 35 | } else { 36 | throw new Exception(format("join conflict between '%s' and '%s'", 37 | a, b)); 38 | } 39 | } 40 | 41 | struct SkipInclude { 42 | Include include; 43 | Skip skip; 44 | 45 | SkipInclude join(SkipInclude other) { 46 | SkipInclude ret = this; 47 | ret.include = add(ret.include, other.include); 48 | ret.skip = add(ret.skip, other.skip); 49 | return ret; 50 | } 51 | } 52 | 53 | bool continueAfterDirectives(const(Directives) dirs, Json vars) { 54 | import graphql.validation.exception; 55 | 56 | SkipInclude si = extractSkipInclude(dirs, vars); 57 | if(si.include == Include.undefined && si.skip == Skip.undefined) { 58 | return true; 59 | } else if(si.include == Include.no && si.skip == Skip.undefined) { 60 | return false; 61 | } else if(si.include == Include.yes 62 | && (si.skip == Skip.undefined || si.skip == Skip.no)) 63 | { 64 | return true; 65 | } else if(si.include == Include.undefined && si.skip == Skip.no) { 66 | return true; 67 | } else if(si.include == Include.undefined && si.skip == Skip.yes) { 68 | return false; 69 | } 70 | throw new ContradictingDirectives(format( 71 | "include %s and skip %s contridict each other", si.include, 72 | si.skip), __FILE__, __LINE__); 73 | } 74 | 75 | SkipInclude extractSkipInclude(const(Directives) dirs, Json vars) { 76 | import graphql.argumentextractor; 77 | 78 | SkipInclude ret; 79 | if(dirs is null) { 80 | return ret; 81 | } 82 | Json args = getArguments(dirs.dir, vars); 83 | immutable bool if_ = args.extract!bool("if"); 84 | ret.include = dirs.dir.name.value == "include" 85 | ? if_ 86 | ? Include.yes 87 | : Include.no 88 | : Include.undefined; 89 | ret.skip = dirs.dir.name.value == "skip" 90 | ? if_ 91 | ? Skip.yes 92 | : Skip.no 93 | : Skip.undefined; 94 | 95 | return ret.join(extractSkipInclude(dirs.follow, vars)); 96 | } 97 | 98 | unittest { 99 | string q = ` 100 | query a($s: boolean) { 101 | starships(overSize: 10) { 102 | name @include(if: $s) 103 | crew @skip(if: $s) { 104 | id 105 | } 106 | } 107 | }`; 108 | 109 | Json vars = parseJsonString(`{ "s": false }`); 110 | 111 | const(Document) doc = lexAndParse(q); 112 | assert(doc !is null); 113 | { 114 | auto name = astSelect!Field(doc, "a.starships.name"); 115 | assert(name !is null); 116 | 117 | SkipInclude si = extractSkipInclude(name.dirs, vars); 118 | assert(si.include == Include.no, format("%s", si.include)); 119 | assert(si.skip == Skip.undefined, format("%s", si.skip)); 120 | assert(!continueAfterDirectives(name.dirs, vars)); 121 | } 122 | { 123 | auto crew = astSelect!Field(doc, "a.starships.crew"); 124 | assert(crew !is null); 125 | 126 | immutable SkipInclude si = extractSkipInclude(crew.dirs, vars); 127 | assert(si.include == Include.undefined); 128 | assert(si.skip == Skip.no); 129 | assert(continueAfterDirectives(crew.dirs, vars)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /source/graphql/exception.d: -------------------------------------------------------------------------------- 1 | module graphql.exception; 2 | 3 | 4 | class ParseException : Exception { 5 | @safe: 6 | 7 | int line; 8 | string[] subRules; 9 | string[] follows; 10 | 11 | this(string msg) { 12 | super(msg); 13 | } 14 | 15 | this(string msg, string f, int l, string[] subRules, string[] follows) { 16 | import std.format : format; 17 | super(format( 18 | "%s [%(%s,%)]: While in subRules [%(%s, %)] at %s:%s", 19 | msg, follows, subRules, f, l), f, l 20 | ); 21 | this.line = l; 22 | this.subRules = subRules; 23 | this.follows = follows; 24 | } 25 | 26 | this(string msg, ParseException other) { 27 | super(msg, other); 28 | } 29 | 30 | this(string msg, ParseException other, string f, int l) { 31 | super(msg, f, l, other); 32 | this.line = l; 33 | } 34 | } 35 | 36 | @safe: 37 | class GQLDExecutionException : Exception { 38 | this(string msg, string f = __FILE__, size_t l = __LINE__) { 39 | super(msg, f, l); 40 | this.line = l; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /source/graphql/package.d: -------------------------------------------------------------------------------- 1 | module graphql; 2 | 3 | public import graphql.graphql; 4 | public import graphql.schema; 5 | public import graphql.argumentextractor; 6 | public import graphql.ast; 7 | public import graphql.builder; 8 | public import graphql.constants; 9 | public import graphql.exception; 10 | public import graphql.helper; 11 | public import graphql.lexer; 12 | public import graphql.parser; 13 | public import graphql.tokenmodule; 14 | public import graphql.traits; 15 | public import graphql.treevisitor; 16 | public import graphql.uda; 17 | public import graphql.visitor; 18 | -------------------------------------------------------------------------------- /source/graphql/parsertests.d: -------------------------------------------------------------------------------- 1 | module graphql.parsertests; 2 | 3 | import std.format : format; 4 | import std.stdio; 5 | 6 | import graphql.lexer; 7 | import graphql.parser; 8 | 9 | private struct TestCase { 10 | int id; 11 | QueryParser qp; 12 | string str; 13 | } 14 | 15 | unittest { 16 | TestCase[] tests; 17 | tests ~= TestCase(0, QueryParser.yes, 18 | ` 19 | mutation updateUser($userId: String! $name: String!) { 20 | updateUser(id: $userId name: $name) { 21 | name 22 | } 23 | } 24 | `); 25 | 26 | tests ~= TestCase(1, QueryParser.yes, ` 27 | query inlineFragmentNoType($expandedInfo: Boolean) { 28 | user(handle: "zuck") { 29 | id 30 | name 31 | ... @include(if: $expandedInfo) { 32 | firstName 33 | lastName 34 | birthday 35 | } 36 | } 37 | } 38 | `); 39 | 40 | tests ~= TestCase(2, QueryParser.yes, ` 41 | query HeroForEpisode($ep: Episode!) { 42 | hero(episode: $ep) { 43 | name 44 | ... on Droid { 45 | primaryFunction 46 | } 47 | ... on Human { 48 | height 49 | } 50 | } 51 | } 52 | `); 53 | 54 | tests ~= TestCase(3, QueryParser.yes, ` 55 | mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { 56 | createReview(episode: $ep, review: $review) { 57 | stars 58 | commentary 59 | } 60 | } 61 | `); 62 | 63 | tests ~= TestCase(4, QueryParser.yes, ` 64 | query HeroNameAndFriends($episode: Episode = "JEDI") { 65 | hero(episode: $episode) { 66 | name 67 | friends { 68 | name 69 | } 70 | } 71 | }`); 72 | 73 | tests ~= TestCase(5, QueryParser.yes, ` 74 | query hero { 75 | name 76 | # Queries can have comments! 77 | 78 | # Queries can have comments Another! 79 | friends { 80 | name 81 | } 82 | }`); 83 | 84 | tests ~= TestCase(6, QueryParser.no, ` 85 | enum DogCommand { SIT, DOWN, HEEL } 86 | 87 | type Dog implements Pet { 88 | name: String! 89 | nickname: String 90 | barkVolume: Int 91 | doesKnowCommand(dogCommand: DogCommand!): Boolean! 92 | isHousetrained(atOtherHomes: Boolean): Boolean! 93 | owner: Human 94 | } 95 | 96 | interface Sentient { 97 | name: String! 98 | } 99 | 100 | interface Pet { 101 | name: String! 102 | } 103 | 104 | type Alien implements Sentient { 105 | name: String! 106 | homePlanet: String 107 | } 108 | 109 | type Human implements Sentient { 110 | name: String! 111 | } 112 | 113 | enum CatCommand { JUMP } 114 | 115 | type Cat implements Pet { 116 | name: String! 117 | nickname: String 118 | doesKnowCommand(catCommand: CatCommand!): Boolean! 119 | meowVolume: Int 120 | } 121 | 122 | union CatOrDog = Cat | Dog 123 | union DogOrHuman = Dog | Human 124 | union HumanOrAlien = Human | Alien 125 | 126 | type QueryRoot { 127 | dog: Dog 128 | } 129 | `); 130 | 131 | tests ~= TestCase(7, QueryParser.no, ` 132 | union SearchResult = Photo | Person 133 | union SearchResult = Photo Person 134 | 135 | type Person { 136 | name: String 137 | age: Int 138 | } 139 | 140 | type Photo { 141 | height: Int 142 | width: Int 143 | } 144 | 145 | type SearchQuery { 146 | firstSearchResult: SearchResult 147 | }`); 148 | 149 | tests ~= TestCase(8, QueryParser.no, ` 150 | interface NamedEntity { 151 | name: String 152 | } 153 | 154 | type Person implements NamedEntity { 155 | name: String 156 | age: Int 157 | } 158 | 159 | type Business implements NamedEntity { 160 | name: String 161 | employeeCount: Int 162 | }`); 163 | 164 | tests ~= TestCase(9, QueryParser.no, `type Person { 165 | name: String 166 | age: Int 167 | picture: Url 168 | }`); 169 | 170 | tests ~= TestCase(10, QueryParser.yes, ` 171 | query myQuery($someTest: Boolean) { 172 | experimentalField @skip(if: $someTest) 173 | }`); 174 | 175 | tests ~= TestCase(11, QueryParser.no, ` 176 | subscription sub { 177 | newMessage { 178 | body 179 | sender 180 | } 181 | } 182 | 183 | fragment newMessageFields on Message { 184 | body 185 | sender 186 | } 187 | 188 | subscription sub { 189 | newMessage { 190 | ... newMessageFields 191 | } 192 | 193 | }`); 194 | 195 | tests ~= TestCase(11, QueryParser.yes, ` 196 | query HeroNameAndFriends($episode: Episode) { 197 | hero(episode: $episode) { 198 | name, 199 | friends { 200 | name 201 | } 202 | } 203 | }`); 204 | 205 | tests ~= TestCase(12, QueryParser.yes, ` 206 | query name { 207 | leftComparison: hero(episode: $EMPIRE) { 208 | ...comparisonFields 209 | } 210 | rightComparison: hero(episode: $JEDI) { 211 | ...comparisonFields 212 | } 213 | } 214 | fragment comparisonFields on Character { 215 | name 216 | appearsIn 217 | friends { 218 | name 219 | } 220 | } 221 | 222 | `); 223 | 224 | tests ~= TestCase(13, QueryParser.yes, ` 225 | query name{ 226 | builds(first: 54) { 227 | all: number 228 | } 229 | } 230 | `); 231 | 232 | tests ~= TestCase(14, QueryParser.yes, ` 233 | query foo { 234 | viewer { 235 | user { 236 | name 237 | builds(first: 1) { 238 | edges { 239 | node { 240 | number 241 | branch 242 | message 243 | } 244 | } 245 | } 246 | } 247 | } 248 | } 249 | `); 250 | 251 | tests ~= TestCase(15, QueryParser.yes, ` 252 | query foo { 253 | name 254 | builds(first: 1) { 255 | abc 256 | } 257 | }`); 258 | 259 | tests ~= TestCase(16, QueryParser.yes, ` 260 | query h { 261 | name 262 | builds 263 | } 264 | `); 265 | 266 | tests ~= TestCase(17, QueryParser.yes, ` 267 | query human($id: H, $limit: lim, $gender: Gen) { 268 | name 269 | height 270 | friends { 271 | id 272 | gender 273 | income 274 | } 275 | }` ); 276 | 277 | tests ~= TestCase(18, QueryParser.yes, ` 278 | query human($id: Var) { 279 | name 280 | height 281 | }`); 282 | 283 | tests ~= TestCase(19, QueryParser.yes, ` 284 | query human() { 285 | name 286 | height 287 | }`); 288 | 289 | tests ~= TestCase(20, QueryParser.yes, ` 290 | query name { 291 | foo 292 | }`); 293 | 294 | /*tests ~= TestCase(21, `{ 295 | query n { 296 | __type(name: "User") { 297 | name 298 | fields { 299 | name 300 | type { 301 | name 302 | } 303 | } 304 | } 305 | } 306 | }`);*/ 307 | 308 | tests ~= TestCase(21, QueryParser.yes, `{ 309 | user(id: 1) { 310 | friends { 311 | name 312 | } 313 | } 314 | }`); 315 | 316 | tests ~= TestCase(22, QueryParser.yes, `query IntrospectionQueryTypeQuery { 317 | __schema { 318 | queryType { 319 | fields { 320 | name 321 | args { 322 | name 323 | description 324 | type { 325 | name 326 | kind 327 | ofType { 328 | name 329 | kind 330 | } 331 | } 332 | defaultValue 333 | } 334 | } 335 | } 336 | } 337 | }`); 338 | 339 | tests ~= TestCase(23, QueryParser.yes, `query IntrospectionDroidFieldsQuery { 340 | __type(name: "Droid") { 341 | name 342 | fields { 343 | name 344 | type { 345 | name 346 | kind 347 | } 348 | } 349 | } 350 | }`); 351 | 352 | tests ~= TestCase(24, QueryParser.yes, ` 353 | query IntrospectionCharacterKindQuery { 354 | __type(name: "Character") { 355 | name 356 | kind 357 | } 358 | }`); 359 | 360 | 361 | tests ~= TestCase(25, QueryParser.yes, `query IntrospectionDroidKindQuery { 362 | __type(name: "Droid") { 363 | name 364 | kind 365 | } 366 | }`); 367 | 368 | tests ~= TestCase(26, QueryParser.yes, `query IntrospectionDroidTypeQuery { 369 | __type(name: "Droid") { 370 | name 371 | } 372 | }`); 373 | 374 | tests ~= TestCase(27, QueryParser.yes, `query IntrospectionQueryTypeQuery { 375 | __schema { 376 | queryType { 377 | name 378 | } 379 | } 380 | }`); 381 | 382 | tests ~= TestCase(28, QueryParser.yes, `query IntrospectionTypeQuery { 383 | __schema { 384 | types { 385 | name 386 | } 387 | } 388 | }`); 389 | 390 | tests ~= TestCase(29, QueryParser.yes, 391 | `query IntrospectionDroidDescriptionQuery { 392 | __type(name: "Droid") { 393 | name 394 | description 395 | } 396 | }`); 397 | 398 | // Issue 20 399 | tests ~= TestCase(30, QueryParser.yes, 400 | `# a stupid comment that crashed Steven's tests 401 | # more comments 402 | query IntrospectionDroidDescriptionQuery { 403 | __type(name: "Droid") { 404 | name 405 | description 406 | } 407 | }`); 408 | 409 | tests ~= TestCase(31, QueryParser.yes, 410 | `# Welcome to GraphiQL 411 | # 412 | # GraphiQL is an in-browser tool for writing, validating, and 413 | # testing GraphQL queries. 414 | # 415 | # Type queries into this side of the screen, and you will see intelligent 416 | # typeaheads aware of the current GraphQL type schema and live syntax and 417 | # validation errors highlighted within the text. 418 | # 419 | # GraphQL queries typically start with a "{" character. Lines that starts 420 | # with a # are ignored. 421 | # 422 | # An example GraphQL query might look like: 423 | # 424 | # { 425 | # field(arg: "value") { 426 | # subField 427 | # } 428 | # } 429 | # 430 | # Keyboard shortcuts: 431 | # 432 | # Prettify Query: Shift-Ctrl-P (or press the prettify button above) 433 | # 434 | # Run Query: Ctrl-Enter (or press the play button above) 435 | # 436 | # Auto Complete: Ctrl-Space (or just start typing) 437 | # 438 | 439 | 440 | {allEmployees 441 | { 442 | info 443 | { 444 | addressInstance 445 | { 446 | line1 447 | } 448 | } 449 | }} 450 | `); 451 | tests ~= TestCase(32, QueryParser.yes, 452 | `query IntrospectionDroidFieldsQuery { 453 | __typenameA 454 | __type(name: "Droid") { 455 | name 456 | fields { 457 | name 458 | type { 459 | name 460 | kind 461 | } 462 | } 463 | } 464 | }`); 465 | 466 | tests ~= TestCase(33, QueryParser.no, ` 467 | """ 468 | Autogenerated input type of ApproveDeployments 469 | """ 470 | input ApproveDeploymentsInput { 471 | """ 472 | A unique identifier for the client performing the mutation. 473 | """ 474 | clientMutationId: String 475 | 476 | """ 477 | Optional comment for approving deployments 478 | """ 479 | comment: String = "" 480 | 481 | """ 482 | The ids of environments to reject deployments 483 | """ 484 | environmentIds: [ID!]! 485 | 486 | """ 487 | The node ID of the workflow run containing the pending deployments. 488 | """ 489 | workflowRunId: ID! @possibleTypes(concreteTypes: ["WorkflowRun"]) 490 | } 491 | `); 492 | 493 | tests ~= TestCase(34, QueryParser.no, ` 494 | type TestType { 495 | input: String! 496 | type: String! 497 | query: String! 498 | } 499 | 500 | input TestInput { 501 | input: String! 502 | type: String! 503 | query: String! 504 | } 505 | `); 506 | 507 | tests ~= TestCase(35, QueryParser.yes, ` 508 | query { 509 | input 510 | type 511 | query 512 | } 513 | `); 514 | 515 | foreach(test; tests) { 516 | auto l = Lexer(test.str, test.qp); 517 | auto p = Parser(l); 518 | try { 519 | auto d = p.parseDocument(); 520 | } catch(Throwable e) { 521 | writeln(e.toString()); 522 | while(e.next) { 523 | writeln(e.next.toString()); 524 | e = e.next; 525 | } 526 | assert(false, format("Test %d", test.id)); 527 | } 528 | assert(p.lex.empty, format("%d %s", test.id, p.lex.getRestOfInput())); 529 | } 530 | } 531 | 532 | unittest { 533 | enum ok = { 534 | auto l = Lexer(` 535 | mutation updateUser($userId: String! $name: String!) { 536 | updateUser(id: $userId name: $name) { 537 | name 538 | } 539 | } 540 | `, QueryParser.yes); 541 | auto p = Parser(l); 542 | auto d = p.parseDocument(); 543 | assert(p.lex.empty, format("%s", p.lex.getRestOfInput())); 544 | return true; 545 | }(); 546 | static assert(ok); 547 | } 548 | 549 | unittest { 550 | import std.file : readText; 551 | auto l = Lexer(readText("starwarsschemaparsetest.graphql"), QueryParser.no); 552 | auto p = Parser(l); 553 | auto a = p.parseDocument(); 554 | } 555 | -------------------------------------------------------------------------------- /source/graphql/schema/directives.d: -------------------------------------------------------------------------------- 1 | module graphql.schema.directives; 2 | 3 | @safe: 4 | 5 | interface DefaultDirectives { 6 | void skip(bool _if); 7 | void include(bool _if); 8 | } 9 | -------------------------------------------------------------------------------- /source/graphql/schema/helper.d: -------------------------------------------------------------------------------- 1 | module graphql.schema.helper; 2 | 3 | import graphql.schema.types; 4 | 5 | @safe: 6 | 7 | string toString(Con)(ref GQLDType!(Con)[string] all) { 8 | import std.array : appender; 9 | import std.format : formattedWrite; 10 | auto app = appender!string(); 11 | foreach(key, value; all) { 12 | formattedWrite(app, "%20s: %s\n", key, value.toString()); 13 | } 14 | return app.data; 15 | } 16 | -------------------------------------------------------------------------------- /source/graphql/schema/introspectiontypes.d: -------------------------------------------------------------------------------- 1 | module graphql.schema.introspectiontypes; 2 | 3 | import std.meta : AliasSeq; 4 | import std.typecons : Nullable; 5 | 6 | import nullablestore; 7 | 8 | import graphql.uda; 9 | 10 | alias IntrospectionTypes = AliasSeq!(__TypeKind, __DirectiveLocation, 11 | __Directive, __EnumValue, __InputValue, __Field, __Type, __Schema); 12 | 13 | struct __Schema { 14 | __Type[] types; 15 | __Type queryType; 16 | Nullable!__Type mutationType; 17 | Nullable!__Type subscriptionType; 18 | __Directive[] directives; 19 | 20 | @GQLDUda(Ignore.yes) 21 | string toString() const { 22 | return "__Schema"; 23 | } 24 | } 25 | 26 | struct __Type { 27 | __TypeKind kind; 28 | Nullable!string name; 29 | Nullable!string description; 30 | 31 | // OBJECT and INTERFACE only 32 | Nullable!(__Field[]) fields(bool includeDeprecated = false); 33 | 34 | // OBJECT only 35 | Nullable!(__Type[]) interfaces; 36 | 37 | // INTERFACE and UNION only 38 | Nullable!(__Type[]) possibleTypes; 39 | 40 | // ENUM only 41 | Nullable!(__EnumValue[]) enumValues(bool includeDeprecated = false); 42 | 43 | // INPUT_OBJECT only 44 | Nullable!(__InputValue[]) inputFields; 45 | 46 | // NON_NULL and LIST only 47 | NullableStore!(__Type) ofType; 48 | 49 | @GQLDUda(Ignore.yes) 50 | string toString() const { 51 | return "__Type"; 52 | } 53 | } 54 | 55 | struct __Field { 56 | string name; 57 | Nullable!string description; 58 | __InputValue[] args; 59 | __Type type; 60 | bool isDeprecated; 61 | Nullable!string deprecationReason; 62 | 63 | @GQLDUda(Ignore.yes) 64 | string toString() const { 65 | return "__Field"; 66 | } 67 | } 68 | 69 | struct __InputValue { 70 | string name; 71 | Nullable!string description; 72 | NullableStore!__Type type; // TODO should not be NullableStore 73 | Nullable!string defaultValue; 74 | 75 | @GQLDUda(Ignore.yes) 76 | string toString() const { 77 | return "__InputValue"; 78 | } 79 | } 80 | 81 | struct __EnumValue { 82 | string name; 83 | Nullable!string description; 84 | bool isDeprecated; 85 | Nullable!string deprecationReason; 86 | 87 | @GQLDUda(Ignore.yes) 88 | string toString() const { 89 | return "__EnumValue"; 90 | } 91 | } 92 | 93 | enum __TypeKind { 94 | SCALAR, 95 | OBJECT, 96 | INTERFACE, 97 | UNION, 98 | ENUM, 99 | INPUT_OBJECT, 100 | LIST, 101 | NON_NULL 102 | } 103 | 104 | struct __Directive { 105 | string name; 106 | Nullable!string description; 107 | __DirectiveLocation[] locations; 108 | __InputValue[] args; 109 | 110 | @GQLDUda(Ignore.yes) 111 | string toString() const { 112 | return "__Directive"; 113 | } 114 | } 115 | 116 | enum __DirectiveLocation { 117 | QUERY, 118 | MUTATION, 119 | SUBSCRIPTION, 120 | FIELD, 121 | FRAGMENT_DEFINITION, 122 | FRAGMENT_SPREAD, 123 | INLINE_FRAGMENT, 124 | SCHEMA, 125 | SCALAR, 126 | OBJECT, 127 | FIELD_DEFINITION, 128 | ARGUMENT_DEFINITION, 129 | INTERFACE, 130 | UNION, 131 | ENUM, 132 | ENUM_VALUE, 133 | INPUT_OBJECT, 134 | INPUT_FIELD_DEFINITION 135 | } 136 | -------------------------------------------------------------------------------- /source/graphql/schema/package.d: -------------------------------------------------------------------------------- 1 | module graphql.schema; 2 | 3 | public import graphql.schema.helper; 4 | public import graphql.schema.resolver; 5 | public import graphql.schema.typeconversions; 6 | public import graphql.schema.types; 7 | -------------------------------------------------------------------------------- /source/graphql/schema/toschemafile.d: -------------------------------------------------------------------------------- 1 | module graphql.schema.toschemafile; 2 | 3 | import std.array; 4 | import std.algorithm.iteration : map, joiner; 5 | import std.algorithm.searching : canFind, startsWith; 6 | import std.conv : to; 7 | import std.stdio; 8 | import std.format; 9 | import std.typecons; 10 | 11 | import graphql.graphql; 12 | import graphql.schema.types; 13 | import graphql.uda; 14 | import graphql.schema.resolver; 15 | import graphql.helper; 16 | 17 | string schemaToString(T)() { 18 | return schemaToString(toSchema!T()); 19 | } 20 | 21 | string schemaToString(T, Q)(GraphQLD!(T, Q) gqld) { 22 | return schemaToString(gqld.schema); 23 | } 24 | 25 | string schemaToString(T)(GQLDSchema!T sch) { 26 | auto app = appender!string(); 27 | TraceType[string] tts; 28 | formIndent(app, 0, "schema {"); 29 | foreach(it; ["mutationType", "queryType", "subscriptionType"]) { 30 | auto tPtr = it in sch.member; 31 | if(tPtr !is null) { 32 | auto t = *tPtr; 33 | formIndent(app, 1, "%s: %s", it[0 .. $ - 4], t.name); 34 | tts[t.name] = TraceType(t, false); 35 | } 36 | } 37 | formIndent(app, 0, "}"); 38 | outer: while(true) { 39 | foreach(key, ref it; tts) { 40 | if(!it.normalDone 41 | || (it.inDone.isNull == false && it.inDone.get() == false) 42 | ) { 43 | toSchemaString(app, it, tts); 44 | it.normalDone = true; 45 | continue outer; 46 | } 47 | } 48 | break outer; 49 | } 50 | return app.data; 51 | } 52 | 53 | struct TraceType { 54 | GQLDType type; 55 | bool normalDone; 56 | Nullable!bool inDone; 57 | } 58 | 59 | private: 60 | 61 | void toSchemaString(Out)(ref Out o, ref TraceType tt, ref TraceType[string] tts) { 62 | if(isPrimitiveType(tt.type) || isNameSpecial(tt.type.name)) { 63 | return; 64 | } 65 | //writefln("%s %s %s", tt.type.name, tt.normalDone, tt.inDone.isNull() 66 | // , !tt.inDone.isNull() ? to!string(tt.inDone.get()) : ""); 67 | if(!tt.normalDone) { 68 | tt.normalDone = true; 69 | if(GQLDEnum e = toEnum(tt.type)) { 70 | formIndent(o, 0, "enum %s {", tt.type.name); 71 | foreach (m; e.memberNames) { 72 | formIndent(o, 1, "%s,", m); 73 | } 74 | formIndent(o, 0, "}"); 75 | } else if(GQLDLeaf l = toLeaf(tt.type)) { 76 | formIndent(o, 0, "scalar %s", l.name); 77 | } else if(GQLDUnion u = toUnion(tt.type)) { 78 | formIndent(o, 0, "union %s = %--(%s | %)", u.name 79 | , u.member.byValue.map!(v => baseType(v).name)); 80 | foreach(it; u.member.byValue) { 81 | addIfNew(tts, it, false); 82 | } 83 | } else if(GQLDMap map = toMap(tt.type)) { 84 | string implementsStr; 85 | if(auto obj = cast(GQLDObject)map) { 86 | if(obj.base && obj.base.typeKind == TypeKind.INTERFACE) { 87 | addIfNew(tts, obj.base, false); 88 | implementsStr = " implements " ~ obj.base.name; 89 | } 90 | } 91 | formIndent(o, 0, "%s %s%s {", schemaTypeIndicator(tt.type) 92 | , tt.type.name, implementsStr); 93 | foreach(memName, mem; allMember(map)) { 94 | string typename = typeToStringMaybeIn(mem, false, false); 95 | if(isNameSpecial(memName)) { 96 | continue; 97 | } 98 | if(GQLDOperation op = toOperation(mem)) { 99 | string rt = op.returnType.gqldTypeToString(); 100 | if(canFind(["[__Field!]", "__Type!"], rt)) { 101 | continue; 102 | } 103 | formIndent(o, 1, "%s%s: %s%s", memName, 104 | op.parameters.keys.length > 0 105 | ? format("(%--(%s, %))", op.parameters.byKeyValue 106 | .map!(kv => format("%s: %s", kv.key, 107 | gqldTypeToStringIn(kv.value)))) 108 | : "" 109 | , op.returnType.gqldTypeToString() 110 | , typeToDeprecationMessage(mem)); 111 | addIfNew(tts, op.returnType, false); 112 | foreach(p; op.parameters.byValue) { 113 | addIfNew(tts, p, true); 114 | } 115 | } else { 116 | formIndent(o, 1, "%s: %s%s", memName, typename 117 | , typeToDeprecationMessage(mem)); 118 | addIfNew(tts, mem, false); 119 | } 120 | } 121 | formIndent(o, 0, "}"); 122 | } 123 | } 124 | if(!tt.inDone.isNull() && tt.inDone.get() == false) { 125 | tt.inDone = nullable(true); 126 | if(tt.type.udaData.typeKind != TypeKind.INPUT_OBJECT 127 | && tt.type.udaData.typeKind != TypeKind.ENUM 128 | && toEnum(tt.type) is null 129 | ) { 130 | if(GQLDMap map = toMap(tt.type)) { 131 | formIndent(o, 0, "input %sIn {", tt.type.name); 132 | foreach(memName, mem; allMember(map)) { 133 | string typename = gqldTypeToStringIn(mem); 134 | if(isNameSpecial(memName)) { 135 | continue; 136 | } 137 | if(GQLDOperation op = toOperation(mem)) { 138 | } else { 139 | formIndent(o, 1, "%s: %s%s", memName, typename 140 | , typeToDeprecationMessage(mem)); 141 | addIfNew(tts, mem, true); 142 | } 143 | } 144 | formIndent(o, 0, "}"); 145 | } 146 | } 147 | } 148 | } 149 | 150 | void addIfNew(ref TraceType[string] tts, GQLDType t, bool isUsedAsInput) { 151 | t = baseType(t); 152 | if(isPrimitiveType(t)) { 153 | return; 154 | } 155 | TraceType* tt = t.name in tts; 156 | if(tt is null) { 157 | tts[t.name] = TraceType(t, false, Nullable!(bool).init); 158 | tt = t.name in tts; 159 | } 160 | if(isUsedAsInput && (*tt).inDone.isNull() == true) { 161 | (*tt).inDone = nullable(false); 162 | } 163 | } 164 | 165 | string schemaTypeIndicator(GQLDType t) { 166 | if(t.udaData.typeKind != TypeKind.UNDEFINED) { 167 | final switch(t.udaData.typeKind) { 168 | case TypeKind.UNDEFINED: return "type"; 169 | case TypeKind.SCALAR: return "scalar"; 170 | case TypeKind.OBJECT: return "type"; 171 | case TypeKind.INTERFACE: return "interface"; 172 | case TypeKind.UNION: return "union"; 173 | case TypeKind.ENUM: return "enum"; 174 | case TypeKind.INPUT_OBJECT: return "input"; 175 | case TypeKind.LIST: return "type"; 176 | case TypeKind.NON_NULL: return "type"; 177 | } 178 | } 179 | if(auto u = toUnion(t)) { 180 | return "union"; 181 | } else if(auto e = toEnum(t)) { 182 | return "enum"; 183 | } 184 | return "type"; 185 | } 186 | 187 | void formIndent(Out, Args...)(ref Out o, size_t indent, string s, Args args) { 188 | foreach(it; 0 .. indent) { 189 | formattedWrite(o, "\t"); 190 | } 191 | formattedWrite(o, s, args); 192 | formattedWrite(o, "\n"); 193 | } 194 | 195 | 196 | bool isPrimitiveType(const(GQLDType) type) { 197 | return type.kind == GQLDKind.String 198 | || type.kind == GQLDKind.Float 199 | || type.kind == GQLDKind.Int 200 | || type.kind == GQLDKind.Bool; 201 | } 202 | 203 | string gqldTypeToString(const(GQLDType) t, string nameSuffix = "", Flag!"nonNullable" nonNullable = Yes.nonNullable) { 204 | if(auto base = cast(const(GQLDNullable))t) { 205 | return gqldTypeToString(base.elementType, nameSuffix, No.nonNullable); 206 | } else if(auto list = cast(const(GQLDList))t) { 207 | return '[' ~ gqldTypeToString(list.elementType, nameSuffix, Yes.nonNullable) ~ ']' ~ (nonNullable ? "!" : ""); 208 | } else if(auto nn = cast(const(GQLDNonNull))t) { 209 | return gqldTypeToString(nn.elementType, nameSuffix, Yes.nonNullable); 210 | } 211 | return t.name ~ nameSuffix ~ (nonNullable ? "!" : ""); 212 | } 213 | 214 | string gqldTypeToStringIn(const(GQLDType) t) { 215 | if(auto base = cast(const(GQLDNullable))t) { 216 | return gqldTypeToStringIn(base.elementType); 217 | } else if(auto list = cast(const(GQLDList))t) { 218 | return '[' ~ gqldTypeToStringIn(list.elementType) ~ ']'; 219 | } else if(auto nn = cast(const(GQLDNonNull))t) { 220 | return gqldTypeToStringIn(nn.elementType) ~ "!"; 221 | } 222 | return canFind([TypeKind.SCALAR, TypeKind.INPUT_OBJECT, TypeKind.ENUM], t.typeKind) 223 | ? t.name 224 | : t.name ~ "In"; 225 | } 226 | 227 | string typeToStringMaybeIn(GQLDType t, bool inputType, bool isParam) { 228 | auto bt = baseType(t); 229 | const bool baseTypeIsNotInputObject = bt.udaData.typeKind != TypeKind.INPUT_OBJECT; 230 | const bool isScalar = (toScalar(bt) !is null); 231 | return gqldTypeToString(t, isParam && !isScalar && !inputType && baseTypeIsNotInputObject 232 | ? "In" 233 | : ""); 234 | } 235 | 236 | string typeToDeprecationMessage(const(GQLDType) t) { 237 | return t.deprecatedInfo.isDeprecated == IsDeprecated.yes 238 | ? ` @deprecated(reason: "` 239 | ~ t.deprecatedInfo.deprecationReason 240 | ~ `")` 241 | : ""; 242 | } 243 | 244 | GQLDType baseType(GQLDType t) { 245 | if(auto base = cast(GQLDNullable)t) { 246 | return baseType(base.elementType); 247 | } else if(auto list = cast(GQLDList)t) { 248 | return baseType(list.elementType); 249 | } else if(auto nn = cast(GQLDNonNull)t) { 250 | return baseType(nn.elementType); 251 | } 252 | return t; 253 | } 254 | -------------------------------------------------------------------------------- /source/graphql/starwars/data.d: -------------------------------------------------------------------------------- 1 | module graphql.starwars.data; 2 | 3 | import std.typecons : Nullable, nullable; 4 | 5 | import graphql.starwars.types; 6 | 7 | @safe: 8 | 9 | Human[string] humanData() { 10 | static Human[string] ret; 11 | static bool created; 12 | if(!created) { 13 | ret["1000"] =new Human("1000", "Luke Skywalker", 14 | ["1002", "1003", "2000", "2001"], [4, 5, 6], "Tatooine" 15 | ); 16 | ret["1001"] = new Human("1001", "Darth Vader", ["1004"], [4, 5, 6], 17 | "Tatooine" 18 | ); 19 | ret["1002"] = new Human("1002", "Han Solo", ["1000", "1003", "2001"], 20 | [4, 5, 6], "" 21 | ); 22 | ret["1003"] = new Human("1003", "Leia Organa", 23 | ["1000", "1002", "2000", "2001"], [4, 5, 6], "Alderaan" 24 | ); 25 | ret["1004"] = new Human("1004", "Wilhuff Tarkin", ["1001"], [4], ""); 26 | created = true; 27 | } 28 | return ret; 29 | } 30 | 31 | Droid[string] droidData() { 32 | static Droid[string] ret; 33 | static bool created; 34 | if(!created) { 35 | ret["2000"] = new Droid("2000", "C-3PO", 36 | ["1000", "1002", "1003", "2001"], [4, 5, 6], "Protocol" 37 | ); 38 | ret["2001"] = new Droid("2001", "R2-D2", ["1000", "1002", "1003"], 39 | [4, 5, 6], "Astromech" 40 | ); 41 | created = true; 42 | } 43 | return ret; 44 | } 45 | 46 | Character getCharacter(string id) { 47 | auto h = id in humanData(); 48 | if(h) { 49 | return *h; 50 | } else { 51 | auto d = id in droidData(); 52 | if(d) { 53 | return *d; 54 | } 55 | } 56 | return null; 57 | } 58 | 59 | Character[] getFriends(Character c) { 60 | import std.array : array; 61 | import std.algorithm.iteration : map; 62 | return c.friendsId.map!(id => getCharacter(id)).array; 63 | } 64 | 65 | Character getHero(Nullable!Episode episode) { 66 | return !episode.isNull() && episode.get() == 5 67 | ? humanData()["1000"] 68 | : droidData()["2001"]; 69 | } 70 | 71 | Human getHuman(string id) { 72 | auto h = id in humanData(); 73 | return h ? *h : null; 74 | } 75 | 76 | Droid getDroid(string id) { 77 | auto d = id in droidData(); 78 | return d ? *d : null; 79 | } 80 | -------------------------------------------------------------------------------- /source/graphql/starwars/introspection.d: -------------------------------------------------------------------------------- 1 | module graphql.starwars.introspection; 2 | 3 | import std.typecons : Nullable, nullable; 4 | import std.format : format; 5 | import std.stdio; 6 | 7 | import vibe.data.json; 8 | 9 | import graphql.constants; 10 | import graphql.parser; 11 | import graphql.builder; 12 | import graphql.lexer; 13 | import graphql.ast; 14 | import graphql.graphql; 15 | import graphql.helper; 16 | import graphql.starwars.data; 17 | import graphql.starwars.schema; 18 | import graphql.starwars.types; 19 | import graphql.validation.querybased; 20 | import graphql.validation.schemabased; 21 | 22 | @safe: 23 | 24 | Json query(string s) { 25 | return query(s, Json.init); 26 | } 27 | 28 | Json query(string s, Json args) { 29 | auto graphqld = new GraphQLD!(StarWarsSchema); 30 | //auto lo = new std.experimental.logger.FileLogger("query.log"); 31 | //graphqld.defaultResolverLog = lo; 32 | //graphqld.executationTraceLog = lo; 33 | //graphqld.resolverLog = lo; 34 | 35 | graphqld.setResolver("queryType", "human", 36 | delegate(string name, Json parent, Json args, 37 | ref DefaultContext con) @safe 38 | { 39 | auto idPtr = "id" in args; 40 | Json ret = Json.emptyObject(); 41 | if(idPtr) { 42 | string id = idPtr.to!string(); 43 | Human h = getHuman(id); 44 | if(h is null) { 45 | ret["data"] = Json(null); 46 | return ret; 47 | } 48 | Json hj = toGraphqlJson(h, graphqld.schema); 49 | Json cj = toGraphqlJson(cast(Character)h, 50 | graphqld.schema); 51 | cj.remove("__typename"); 52 | ret["data"] = joinJson(hj, cj); 53 | } 54 | return ret; 55 | } 56 | ); 57 | 58 | graphqld.setResolver("queryType", "hero", 59 | delegate(string name, Json parent, Json args, 60 | ref DefaultContext con) @safe 61 | { 62 | import std.conv : to; 63 | auto e = "episode" in args; 64 | Json ret = Json.emptyObject(); 65 | ret["data"] = toGraphqlJson(getHero( 66 | e ? nullable((*e).to!string().to!Episode()) 67 | : Nullable!(Episode).init 68 | ), graphqld.schema); 69 | return ret; 70 | } 71 | ); 72 | 73 | graphqld.setResolver("Character", "secretBackstory", 74 | delegate(string name, Json parent, Json args, 75 | ref DefaultContext con) @safe 76 | { 77 | Json ret = Json.emptyObject(); 78 | ret[Constants.data] = Json(null); 79 | ret[Constants.errors] = Json.emptyArray(); 80 | ret.insertError("secretBackstory is secret"); 81 | return ret; 82 | } 83 | ); 84 | 85 | graphqld.setResolver("Character", "friends", 86 | delegate(string name, Json parent, Json args, 87 | ref DefaultContext con) @safe 88 | { 89 | auto idPtr = "id" in parent; 90 | Json ret = Json.emptyObject(); 91 | ret["data"] = Json.emptyArray(); 92 | if(idPtr) { 93 | string id = idPtr.to!string(); 94 | foreach(it; getFriends(getCharacter(id))) { 95 | ret["data"] ~= toGraphqlJson(it, graphqld.schema); 96 | } 97 | } 98 | return ret; 99 | } 100 | ); 101 | 102 | auto l = Lexer(s); 103 | auto p = Parser(l); 104 | 105 | Document d = p.parseDocument(); 106 | const(Document) cd = d; 107 | QueryValidator fv = new QueryValidator(d); 108 | fv.accept(cd); 109 | noCylces(fv.fragmentChildren); 110 | allFragmentsReached(fv); 111 | SchemaValidator!StarWarsSchema sv = new SchemaValidator!StarWarsSchema(d, 112 | graphqld.schema 113 | ); 114 | sv.accept(cd); 115 | DefaultContext con; 116 | Json gqld = graphqld.execute(d, args, con); 117 | return gqld; 118 | } 119 | 120 | @safe unittest { 121 | Json rslt = query(` 122 | query IntrospectionTypeQuery { 123 | __schema { 124 | types { 125 | name 126 | } 127 | } 128 | } 129 | `); 130 | 131 | string s = ` { 132 | "data": { 133 | "__schema": { 134 | "types": [ 135 | { 136 | "name": "Boolean" 137 | }, 138 | { 139 | "name": "Character" 140 | }, 141 | { 142 | "name": "Droid" 143 | }, 144 | { 145 | "name": "Episode" 146 | }, 147 | { 148 | "name": "Float" 149 | }, 150 | { 151 | "name": "Human" 152 | }, 153 | { 154 | "name": "Int" 155 | }, 156 | { 157 | "name": "String" 158 | }, 159 | { 160 | "name": "__InputValue" 161 | }, 162 | { 163 | "name": "__Type" 164 | }, 165 | { 166 | "name": "__TypeKind" 167 | }, 168 | { 169 | "name": "queryType" 170 | }, 171 | { 172 | "name": "subscriptionType" 173 | } 174 | ] 175 | } 176 | } 177 | }`; 178 | Json exp = parseJson(s); 179 | auto cmpResult = compareJson(exp, rslt, "", true); 180 | // TODO make this test pass 181 | assert(cmpResult.okay, format("msg: %s\npath: %--(%s,%)\nexp:\n%s\ngot:\n%s" 182 | , cmpResult.message, cmpResult.path, exp.toPrettyString() 183 | , rslt.toPrettyString())); 184 | //if(!cmpResult.okay) { 185 | // writefln("msg: %s\npath: %--(%s,%)\nexp:\n%s\ngot:\n%s" 186 | // , cmpResult.message, cmpResult.path, exp.toPrettyString() 187 | // , rslt.toPrettyString()); 188 | //} 189 | } 190 | 191 | @safe unittest { 192 | Json rslt = query(` 193 | query IntrospectionDroidTypeQuery { 194 | __type(name: "Droid") { 195 | name 196 | } 197 | } 198 | `); 199 | 200 | string s = `{ 201 | "data" : { 202 | "__type" : { 203 | "name" : "Droid" 204 | } 205 | } 206 | } 207 | `; 208 | Json exp = parseJson(s); 209 | assert(rslt == exp, format("exp:\n%s\ngot:\n%s", exp.toPrettyString(), 210 | rslt.toPrettyString())); 211 | } 212 | 213 | @safe unittest { 214 | Json rslt = query(` 215 | query IntrospectionQueryTypeQuery { 216 | __schema { 217 | queryType { 218 | name 219 | } 220 | } 221 | } 222 | `); 223 | 224 | string s = `{ 225 | "data" : { 226 | "__schema" : { 227 | "queryType" : { 228 | "name" : "queryType" 229 | } 230 | } 231 | } 232 | } 233 | `; 234 | Json exp = parseJson(s); 235 | assert(rslt == exp, format("exp:\n%s\ngot:\n%s", exp.toPrettyString(), 236 | rslt.toPrettyString())); 237 | } 238 | 239 | @safe unittest { 240 | Json rslt = query(` 241 | query IntrospectionDroidTypeQuery { 242 | __type(name: "Droid") { 243 | name 244 | kind 245 | } 246 | } 247 | `); 248 | 249 | string s = `{ 250 | "data" : { 251 | "__type" : { 252 | "name" : "Droid", 253 | "kind" : "OBJECT" 254 | } 255 | } 256 | } 257 | `; 258 | Json exp = parseJson(s); 259 | assert(rslt == exp, format("exp:\n%s\ngot:\n%s", exp.toPrettyString(), 260 | rslt.toPrettyString())); 261 | } 262 | 263 | @safe unittest { 264 | Json rslt = query(` 265 | query IntrospectionCharacterKindQuery { 266 | __type(name: "Character") { 267 | name 268 | kind 269 | } 270 | } 271 | `); 272 | 273 | string s = `{ 274 | "data" : { 275 | "__type" : { 276 | "name" : "Character", 277 | "kind" : "INTERFACE" 278 | } 279 | } 280 | } 281 | `; 282 | Json exp = parseJson(s); 283 | assert(rslt == exp, format("exp:\n%s\ngot:\n%s", exp.toPrettyString(), 284 | rslt.toPrettyString())); 285 | } 286 | 287 | @safe unittest { 288 | Json rslt = query(` 289 | query IntrospectionDroidFieldsQuery { 290 | __type(name: "Droid") { 291 | name 292 | fields { 293 | name 294 | type { 295 | name 296 | kind 297 | } 298 | } 299 | } 300 | } 301 | `); 302 | 303 | string s = ` 304 | { 305 | "data": { 306 | "__type": { 307 | "fields": [ 308 | { 309 | "type": { 310 | "kind": "SCALAR", 311 | "name": "String" 312 | }, 313 | "name": "name" 314 | }, 315 | { 316 | "type": { 317 | "kind": "LIST", 318 | "name": null 319 | }, 320 | "name": "friends" 321 | }, 322 | { 323 | "type": { 324 | "kind": "SCALAR", 325 | "name": "String" 326 | }, 327 | "name": "secretBackstory" 328 | }, 329 | { 330 | "type": { 331 | "kind": "SCALAR", 332 | "name": "String" 333 | }, 334 | "name": "primaryFunction" 335 | }, 336 | { 337 | "type": { 338 | "kind": "NON_NULL", 339 | "name": null 340 | }, 341 | "name": "id" 342 | }, 343 | { 344 | "type": { 345 | "kind": "LIST", 346 | "name": null 347 | }, 348 | "name": "appearsIn" 349 | } 350 | ], 351 | "name": "Droid" 352 | } 353 | } 354 | } 355 | `; 356 | Json exp = parseJson(s); 357 | auto r = compareJson(exp, rslt, "", true); 358 | assert(r.okay, format("msg: %s\nexp:\n%s\ngot:\n%s", r 359 | , exp.toPrettyString(), rslt.toPrettyString())); 360 | } 361 | 362 | @safe unittest { 363 | Json rslt = query(` 364 | query IntrospectionDroidNestedFieldsQuery { 365 | __type(name: "Droid") { 366 | name 367 | fields { 368 | name 369 | type { 370 | name 371 | kind 372 | ofType { 373 | name 374 | kind 375 | } 376 | } 377 | } 378 | } 379 | } 380 | `); 381 | 382 | string s = `{ 383 | "data" : { 384 | "__type" : { 385 | "name" : "Droid", 386 | "fields" : [ 387 | { 388 | "name": "primaryFunction", 389 | "type": { 390 | "name": "String", 391 | "kind": "SCALAR", 392 | "ofType": null 393 | } 394 | }, 395 | { 396 | "name": "id", 397 | "type": { 398 | "name": null, 399 | "kind": "NON_NULL", 400 | "ofType": { 401 | "name": "String", 402 | "kind": "SCALAR" 403 | } 404 | } 405 | }, 406 | { 407 | "name": "name", 408 | "type": { 409 | "name": "String", 410 | "kind": "SCALAR", 411 | "ofType": null 412 | } 413 | }, 414 | { 415 | "name": "friends", 416 | "type": { 417 | "name": null, 418 | "kind": "LIST", 419 | "ofType": { 420 | "name": "Character", 421 | "kind": "INTERFACE" 422 | } 423 | } 424 | }, 425 | { 426 | "name": "appearsIn", 427 | "type": { 428 | "name": null, 429 | "kind": "LIST", 430 | "ofType": { 431 | "name": "Episode", 432 | "kind": "ENUM" 433 | } 434 | } 435 | }, 436 | { 437 | "name": "secretBackstory", 438 | "type": { 439 | "name": "String", 440 | "kind": "SCALAR", 441 | "ofType": null 442 | } 443 | }, 444 | ] 445 | } 446 | } 447 | } 448 | `; 449 | Json exp = parseJson(s); 450 | auto r = compareJson(exp, rslt, "", true); 451 | assert(r.okay, format("msg: %s\nexp:\n%s\ngot:\n%s" 452 | , r.message 453 | , exp.toPrettyString() 454 | , rslt.toPrettyString())); 455 | } 456 | 457 | @safe unittest { 458 | Json rslt = query(` 459 | query IntrospectionQueryTypeQuery { 460 | __schema { 461 | queryType { 462 | fields { 463 | name 464 | args { 465 | name 466 | description 467 | type { 468 | name 469 | kind 470 | ofType { 471 | name 472 | kind 473 | } 474 | } 475 | defaultValue 476 | } 477 | } 478 | } 479 | } 480 | } 481 | `); 482 | 483 | string s = ` { 484 | "data" : { 485 | "__schema": { 486 | "queryType": { 487 | "fields": [ 488 | { 489 | "name": "droid", 490 | "args": [ 491 | { 492 | "name": "id", 493 | "description": "id of the droid", 494 | "type": { 495 | "kind": "NON_NULL", 496 | "name": null, 497 | "ofType": { 498 | "kind": "SCALAR", 499 | "name": "String" 500 | } 501 | }, 502 | "defaultValue": null 503 | } 504 | ] 505 | }, 506 | { 507 | "args": [ 508 | { 509 | "description": "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.", 510 | "type": { 511 | "kind": "ENUM", 512 | "name": "Episode", 513 | "ofType": null 514 | }, 515 | "defaultValue": null, 516 | "name": "episode", 517 | } 518 | ], 519 | "name": "hero" 520 | }, 521 | { 522 | "name": "human", 523 | "args": [ 524 | { 525 | "name": "id", 526 | "description": "id of the human", 527 | "type": { 528 | "kind": "NON_NULL", 529 | "name": null, 530 | "ofType": { 531 | "kind": "SCALAR", 532 | "name": "String" 533 | } 534 | }, 535 | "defaultValue": null 536 | } 537 | ] 538 | }, 539 | { 540 | "args": [ 541 | { 542 | "description": "", 543 | "type": { 544 | "kind": "SCALAR", 545 | "ofType": null, 546 | "name": "Boolean" 547 | }, 548 | "defaultValue": null, 549 | "name": "includeDeprecated" 550 | } 551 | ], 552 | "name": "fields" 553 | }, 554 | { 555 | "args": [], 556 | "name": "name" 557 | } 558 | ] 559 | } 560 | } 561 | } 562 | }`; 563 | Json exp = parseJson(s); 564 | auto r = compareJson(exp, rslt, "", true); 565 | string extS = exp.toPrettyString(); 566 | string rsltS = rslt.toPrettyString(); 567 | assert(r.okay, format("msg: %s\nexp:\n%s\ngot:\n%s", r.message, extS, rsltS)); 568 | } 569 | 570 | @safe unittest { 571 | Json rslt = query(` 572 | { 573 | __type(name: "Droid") { 574 | name 575 | description 576 | } 577 | }`); 578 | 579 | string s = `{ 580 | "data" : { 581 | "__type": { 582 | "name" : "Droid", 583 | "description" : "A mechanical creature in the Star Wars universe." 584 | } 585 | } 586 | }`; 587 | Json exp = parseJson(s); 588 | auto r = compareJson(exp, rslt, "", true); 589 | string extS = exp.toPrettyString(); 590 | string rsltS = rslt.toPrettyString(); 591 | assert(r.okay, format("msg: %s\nexp:\n%s\ngot:\n%s", r.message, extS, rsltS)); 592 | } 593 | 594 | @safe unittest { 595 | Json rslt = query(` 596 | { 597 | __type(name: "Character") { 598 | fields { 599 | name 600 | description 601 | } 602 | } 603 | }`); 604 | 605 | string s = `{ 606 | "data": { 607 | "__type": { 608 | "fields": [ 609 | { 610 | "description": "The id of the character", 611 | "name": "id" 612 | }, 613 | { 614 | "description": "The name of the character", 615 | "name": "name" 616 | }, 617 | { 618 | "description": "The friends of the character, or an empty list if they have none.", 619 | "name": "friends" 620 | }, 621 | { 622 | "description": "Which movies they appear in.", 623 | "name": "appearsIn" 624 | }, 625 | { 626 | "description": "Where are they from and how they came to be who they are.", 627 | "name": "secretBackstory" 628 | } 629 | ] 630 | } 631 | } 632 | }`; 633 | Json exp = parseJson(s); 634 | auto r = compareJson(exp, rslt, "", true); 635 | string extS = exp.toPrettyString(); 636 | string rsltS = rslt.toPrettyString(); 637 | assert(r.okay, format("msg: %s\nexp:\n%s\ngot:\n%s", r.message, extS, rsltS)); 638 | } 639 | -------------------------------------------------------------------------------- /source/graphql/starwars/query.d: -------------------------------------------------------------------------------- 1 | module graphql.starwars.query; 2 | 3 | import std.typecons : Nullable, nullable; 4 | import std.format : format; 5 | import std.stdio; 6 | 7 | import vibe.data.json; 8 | 9 | import graphql.constants; 10 | import graphql.parser; 11 | import graphql.builder; 12 | import graphql.lexer; 13 | import graphql.ast; 14 | import graphql.graphql; 15 | import graphql.exception; 16 | import graphql.helper; 17 | import graphql.starwars.data; 18 | import graphql.starwars.schema; 19 | import graphql.starwars.types; 20 | import graphql.validation.querybased; 21 | import graphql.validation.schemabased; 22 | 23 | @safe: 24 | 25 | Json query(string s) { 26 | return query(s, Json.init); 27 | } 28 | 29 | Json query(string s, Json args) { 30 | auto graphqld = new GraphQLD!(StarWarsSchema); 31 | //auto lo = new std.experimental.logger.FileLogger("query.log"); 32 | //graphqld.defaultResolverLog = lo; 33 | //graphqld.executationTraceLog = lo; 34 | 35 | graphqld.setResolver("queryType", "human", 36 | delegate(string name, Json parent, Json args, 37 | ref DefaultContext con) @safe 38 | { 39 | auto idPtr = "id" in args; 40 | Json ret = Json.emptyObject(); 41 | if(idPtr) { 42 | string id = idPtr.to!string(); 43 | Human h = getHuman(id); 44 | if(h is null) { 45 | ret["data"] = Json(null); 46 | return ret; 47 | } 48 | Json hj = toGraphqlJson(h, graphqld.schema); 49 | Json cj = toGraphqlJson(cast(Character)h, graphqld.schema); 50 | cj.remove("__typename"); 51 | ret["data"] = joinJson(hj, cj); 52 | } 53 | return ret; 54 | } 55 | ); 56 | 57 | graphqld.setResolver("queryType", "hero", 58 | delegate(string name, Json parent, Json args, 59 | ref DefaultContext con) @safe 60 | { 61 | import std.conv : to; 62 | auto e = "episode" in args; 63 | Json ret = Json.emptyObject(); 64 | Character c = getHero(e 65 | ? nullable((*e).to!string().to!Episode()) 66 | : Nullable!(Episode).init); 67 | if(Human h = cast(Human)c) { 68 | ret["data"] = toGraphqlJson(h, graphqld.schema); 69 | } else if(Droid d = cast(Droid)c) { 70 | ret["data"] = toGraphqlJson(d, graphqld.schema); 71 | } else { 72 | ret["data"] = toGraphqlJson(c, graphqld.schema); 73 | } 74 | return ret; 75 | } 76 | ); 77 | 78 | graphqld.setResolver("Character", "secretBackstory", 79 | delegate(string name, Json parent, Json args, 80 | ref DefaultContext con) @safe 81 | { 82 | Json ret; 83 | if(name == "secretBackstory") { 84 | throw new GQLDExecutionException("secretBackstory is secret"); 85 | } 86 | return ret; 87 | } 88 | ); 89 | 90 | graphqld.setResolver("Character", "friends", 91 | delegate(string name, Json parent, Json args, 92 | ref DefaultContext con) @safe 93 | { 94 | auto idPtr = "id" in parent; 95 | Json ret = Json.emptyObject(); 96 | ret["data"] = Json.emptyArray(); 97 | if(idPtr) { 98 | string id = idPtr.to!string(); 99 | foreach(it; getFriends(getCharacter(id))) { 100 | ret["data"] ~= toGraphqlJson(it, graphqld.schema); 101 | } 102 | } 103 | return ret; 104 | } 105 | ); 106 | 107 | auto l = Lexer(s); 108 | auto p = Parser(l); 109 | 110 | Document d = p.parseDocument(); 111 | const(Document) cd = d; 112 | QueryValidator fv = new QueryValidator(d); 113 | fv.accept(cd); 114 | noCylces(fv.fragmentChildren); 115 | allFragmentsReached(fv); 116 | SchemaValidator!StarWarsSchema sv = new SchemaValidator!StarWarsSchema(d, 117 | graphqld.schema 118 | ); 119 | sv.accept(cd); 120 | DefaultContext con; 121 | Json gqld = graphqld.execute(d, args, con); 122 | return gqld; 123 | } 124 | 125 | @safe unittest { 126 | Json rslt = query( 127 | `query HeroNameQuery { 128 | hero { 129 | name 130 | } 131 | }`); 132 | 133 | string s = `{ "data" : { "hero" : { "name" : "R2-D2" } } }`; 134 | Json exp = parseJson(s); 135 | assert(rslt == exp, rslt.toPrettyString() ~ "\n" ~ exp.toPrettyString()); 136 | } 137 | 138 | @safe unittest { 139 | Json rslt = query( 140 | `query HeroNameQuery { 141 | hero { 142 | name 143 | } 144 | }`); 145 | string s = `{ "data" : { "hero" : { "name" : "R2-D2" } } }`; 146 | Json exp = parseJson(s); 147 | assert(rslt == exp, rslt.toPrettyString() ~ "\n" ~ exp.toPrettyString()); 148 | } 149 | 150 | @safe unittest { 151 | Json rslt = query( 152 | `query HeroNameAndFriendsQuery { 153 | hero { 154 | id 155 | name 156 | friends { 157 | name 158 | } 159 | } 160 | }`); 161 | string s = `{ 162 | "data": { 163 | "hero": { 164 | "id": "2001", 165 | "name": "R2-D2", 166 | "friends": [ 167 | { 168 | "name": "Luke Skywalker", 169 | }, 170 | { 171 | "name": "Han Solo", 172 | }, 173 | { 174 | "name": "Leia Organa", 175 | } 176 | ] 177 | } 178 | } 179 | }`; 180 | Json exp = parseJson(s); 181 | assert(rslt == exp, rslt.toPrettyString() ~ "\n" ~ exp.toPrettyString()); 182 | } 183 | 184 | @safe unittest { 185 | Json rslt = query(` 186 | query NestedQuery { 187 | hero { 188 | name 189 | friends { 190 | name 191 | appearsIn 192 | friends { 193 | name 194 | } 195 | } 196 | } 197 | }`); 198 | 199 | string s = ` 200 | { 201 | "data": { 202 | "hero": { 203 | "name": "R2-D2", 204 | "friends": [ 205 | { 206 | "name": "Luke Skywalker", 207 | "appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"], 208 | "friends": [ 209 | { "name": "Han Solo", }, 210 | { "name": "Leia Organa", }, 211 | { "name": "C-3PO", }, 212 | { "name": "R2-D2", }, 213 | ], 214 | }, 215 | { 216 | "name": "Han Solo", 217 | "appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"], 218 | "friends": [ 219 | { "name": "Luke Skywalker", }, 220 | { "name": "Leia Organa", }, 221 | { "name": "R2-D2", }, 222 | ], 223 | }, 224 | { 225 | "name": "Leia Organa", 226 | "appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"], 227 | "friends": [ 228 | { "name": "Luke Skywalker", }, 229 | { "name": "Han Solo", }, 230 | { "name": "C-3PO", }, 231 | { "name": "R2-D2", }, 232 | ], 233 | }, 234 | ], 235 | }, 236 | } 237 | }`; 238 | Json exp = parseJson(s); 239 | assert(rslt == exp, rslt.toPrettyString() ~ "\n" ~ exp.toPrettyString()); 240 | } 241 | 242 | @safe unittest { 243 | Json rslt = query(` 244 | query FetchLukeQuery { 245 | human(id: "1000") { 246 | name 247 | } 248 | }`); 249 | 250 | string s = `{ "data" : { "human" : { "name" : "Luke Skywalker" } } }`; 251 | Json exp = parseJson(s); 252 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 253 | exp.toPrettyString(), rslt.toPrettyString())); 254 | } 255 | 256 | @safe unittest { 257 | string args = `{"a": false}`; 258 | Json rslt = query(` 259 | query FetchLukeQuery($a: Boolean!) { 260 | human(id: "1000") @include(if: $a) { 261 | name 262 | } 263 | }`, parseJson(args)); 264 | 265 | string s = `{ "data" : { } } }`; 266 | Json exp = parseJson(s); 267 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 268 | exp.toPrettyString(), rslt.toPrettyString())); 269 | } 270 | 271 | @safe unittest { 272 | Json rslt = query(` 273 | query FetchLukeQuery() { 274 | human(id: "1000") @include(if: false) { 275 | name 276 | } 277 | }`); 278 | 279 | string s = `{ "data" : { } } }`; 280 | Json exp = parseJson(s); 281 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 282 | exp.toPrettyString(), rslt.toPrettyString())); 283 | } 284 | 285 | @safe unittest { 286 | Json rslt = query(` 287 | query FetchLukeQuery() { 288 | human(id: "1000") @include(if: true) { 289 | name 290 | } 291 | }`); 292 | 293 | string s = `{ "data" : { "human" : { "name" : "Luke Skywalker" } } }`; 294 | Json exp = parseJson(s); 295 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 296 | exp.toPrettyString(), rslt.toPrettyString())); 297 | } 298 | 299 | @safe unittest { 300 | string args = `{"a": true}`; 301 | Json rslt = query(` 302 | query FetchLukeQuery($a: Boolean!) { 303 | human(id: "1000") @include(if: $a) { 304 | name 305 | } 306 | }`, parseJson(args)); 307 | 308 | string s = `{ "data" : { "human" : { "name" : "Luke Skywalker" } } }`; 309 | Json exp = parseJson(s); 310 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 311 | exp.toPrettyString(), rslt.toPrettyString())); 312 | } 313 | 314 | @safe unittest { 315 | auto args = `{"someId": "1000"}`; 316 | Json rslt = query(` 317 | query FetchSomeIDQuery($someId: String!) { 318 | human(id: $someId) { 319 | name 320 | } 321 | }`, parseJson(args)); 322 | 323 | string s = `{ "data" : { "human" : { "name" : "Luke Skywalker" } } }`; 324 | Json exp = parseJson(s); 325 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 326 | exp.toPrettyString(), rslt.toPrettyString())); 327 | } 328 | 329 | @safe unittest { 330 | auto args = `{"someId": "1002"}`; 331 | Json rslt = query(` 332 | query FetchSomeIDQuery($someId: String!) { 333 | human(id: $someId) { 334 | name 335 | } 336 | }`, parseJson(args)); 337 | 338 | string s = `{ "data" : { "human" : { "name" : "Han Solo" } } }`; 339 | Json exp = parseJson(s); 340 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 341 | exp.toPrettyString(), rslt.toPrettyString())); 342 | } 343 | 344 | @safe unittest { 345 | auto args = `{ "id" : "not a valid id" }`; 346 | Json rslt = query(` 347 | query humanQuery($id: String!) { 348 | human(id: $id) { 349 | name 350 | } 351 | }`, parseJson(args)); 352 | 353 | string s = `{ "data" : { "human" : null } }`; 354 | Json exp = parseJson(s); 355 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 356 | exp.toPrettyString(), rslt.toPrettyString())); 357 | } 358 | 359 | @safe unittest { 360 | Json rslt = query(` 361 | query FetchLukeAliased { 362 | luke: human(id: "1000") { 363 | name 364 | } 365 | }`); 366 | 367 | string s = `{ "data" : { "luke" : { "name" : "Luke Skywalker" } } }`; 368 | Json exp = parseJson(s); 369 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 370 | exp.toPrettyString(), rslt.toPrettyString())); 371 | } 372 | 373 | @safe unittest { 374 | Json rslt = query(` 375 | query FetchLukeAndLeiaAliased { 376 | luke: human(id: "1000") { 377 | name 378 | } 379 | leia: human(id: "1003") { 380 | name 381 | } 382 | }`); 383 | 384 | string s = `{ "data" : { "luke" : { "name" : "Luke Skywalker" }, 385 | "leia" : { "name" : "Leia Organa" } } }`; 386 | Json exp = parseJson(s); 387 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 388 | exp.toPrettyString(), rslt.toPrettyString())); 389 | } 390 | 391 | @safe unittest { 392 | Json rslt = query(` 393 | query DuplicateFields { 394 | luke: human(id: "1000") { 395 | name 396 | homePlanet 397 | } 398 | leia: human(id: "1003") { 399 | name 400 | homePlanet 401 | } 402 | }`); 403 | 404 | string s = `{ "data" : 405 | { "luke" : 406 | { "name" : "Luke Skywalker", "homePlanet" : "Tatooine" } 407 | , "leia" : 408 | { "name" : "Leia Organa", "homePlanet" : "Alderaan" } 409 | } 410 | }`; 411 | Json exp = parseJson(s); 412 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 413 | exp.toPrettyString(), rslt.toPrettyString())); 414 | } 415 | 416 | @safe unittest { 417 | Json rslt = query(` 418 | query UseFragment { 419 | luke: human(id: "1000") { 420 | ...HumanFragment 421 | } 422 | leia: human(id: "1003") { 423 | ...HumanFragment 424 | } 425 | } 426 | fragment HumanFragment on Human { 427 | name 428 | homePlanet 429 | }`); 430 | 431 | string s = `{ "data" : 432 | { "luke" : 433 | { "name" : "Luke Skywalker", "homePlanet" : "Tatooine" } 434 | , "leia" : 435 | { "name" : "Leia Organa", "homePlanet" : "Alderaan" } 436 | } 437 | }`; 438 | Json exp = parseJson(s); 439 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 440 | exp.toPrettyString(), rslt.toPrettyString())); 441 | } 442 | 443 | @safe unittest { 444 | Json rslt = query(` 445 | query CheckTypeOfR2 { 446 | hero { 447 | __typename 448 | name 449 | } 450 | }`); 451 | 452 | string s = `{"data" : { "hero" : { "__typename": "Droid", "name": "R2-D2" } 453 | } }`; 454 | Json exp = parseJson(s); 455 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 456 | exp.toPrettyString(), rslt.toPrettyString())); 457 | } 458 | 459 | @safe unittest { 460 | Json rslt = query(` 461 | query CheckTypeOfLuke { 462 | hero(episode: EMPIRE) { 463 | __typename 464 | name 465 | } 466 | }`); 467 | 468 | string s = `{"data" : { "hero" : { "__typename": "Human", 469 | "name": "Luke Skywalker" } 470 | } }`; 471 | Json exp = parseJson(s); 472 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 473 | exp.toPrettyString(), rslt.toPrettyString())); 474 | } 475 | 476 | @safe unittest { 477 | Json rslt = query(` 478 | query HeroNameQuery { 479 | hero { 480 | name 481 | secretBackstory 482 | } 483 | }`); 484 | 485 | string s = `{"data" : { "hero" : { "name": "R2-D2", 486 | "secretBackstory": null, } }, "errors" : [ { 487 | "path": [ "HeroNameQuery", "hero", "secretBackstory"], 488 | "message": "secretBackstory is secret" } ]}`; 489 | Json exp = parseJson(s); 490 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 491 | exp.toPrettyString(), rslt.toPrettyString())); 492 | } 493 | 494 | @safe unittest { 495 | Json rslt = query(` 496 | query HeroNameQuery { 497 | hero { 498 | name 499 | friends { 500 | name 501 | secretBackstory 502 | } 503 | } 504 | }`); 505 | 506 | string s = `{ 507 | "errors" : [ 508 | { "message": "secretBackstory is secret" 509 | , "path": [ "HeroNameQuery", "hero", "friends", 0, "secretBackstory"] }, 510 | { "message": "secretBackstory is secret" 511 | , "path": [ "HeroNameQuery", "hero", "friends", 1, "secretBackstory"] }, 512 | { "message": "secretBackstory is secret" 513 | , "path": [ "HeroNameQuery", "hero", "friends", 2, "secretBackstory"] } 514 | ], 515 | "data": { "hero": { "name": "R2-D2", 516 | "friends": [ 517 | { "name": "Luke Skywalker", "secretBackstory": null }, 518 | { "name": "Han Solo", "secretBackstory": null }, 519 | { "name": "Leia Organa", "secretBackstory": null } 520 | ] 521 | } 522 | } 523 | }`; 524 | Json exp = parseJson(s); 525 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 526 | exp.toPrettyString(), rslt.toPrettyString())); 527 | } 528 | 529 | @safe unittest { 530 | Json rslt = query(` 531 | query HeroNameQuery { 532 | mainHero: hero { 533 | name 534 | story: secretBackstory 535 | } 536 | }`); 537 | 538 | string s = `{ 539 | "data": { 540 | "mainHero": { 541 | "name": "R2-D2", 542 | "story": null, 543 | }, 544 | }, 545 | "errors" : [ 546 | { "message": "secretBackstory is secret" 547 | , "path": [ "HeroNameQuery", "mainHero", "story"] } 548 | ] 549 | }`; 550 | Json exp = parseJson(s); 551 | assert(rslt == exp, format("\nexp:\n%s\ngot:\n%s", 552 | exp.toPrettyString(), rslt.toPrettyString())); 553 | } 554 | -------------------------------------------------------------------------------- /source/graphql/starwars/schema.d: -------------------------------------------------------------------------------- 1 | module graphql.starwars.schema; 2 | 3 | import std.typecons : Nullable, nullable; 4 | 5 | import graphql.starwars.data; 6 | import graphql.starwars.types; 7 | import graphql.uda; 8 | 9 | @safe: 10 | 11 | @GQLDUda(TypeKind.OBJECT) 12 | struct StarWarsQuery { 13 | Nullable!Character hero( 14 | @GQLDUda(GQLDDescription("If omitted, returns the hero of the " 15 | ~ "whole saga. If provided, returns the hero of that " 16 | ~ "particular episode.")) 17 | Nullable!Episode episode 18 | ); 19 | Nullable!Human human( 20 | @GQLDUda(GQLDDescription("id of the human")) string id 21 | ); 22 | Nullable!Droid droid( 23 | @GQLDUda(GQLDDescription("id of the droid")) string id 24 | ); 25 | } 26 | 27 | struct StarWarsSubscription { 28 | } 29 | 30 | class StarWarsSchema { 31 | StarWarsQuery queryType; 32 | StarWarsSubscription subscriptionType; 33 | } 34 | -------------------------------------------------------------------------------- /source/graphql/starwars/types.d: -------------------------------------------------------------------------------- 1 | module graphql.starwars.types; 2 | 3 | import std.typecons : Nullable, nullable; 4 | 5 | import nullablestore; 6 | 7 | import graphql.uda; 8 | 9 | @safe: 10 | 11 | @GQLDUda( 12 | GQLDDescription("One of the films in the Star Wars Trilogy") 13 | ) 14 | enum Episode { 15 | NEWHOPE = 4, 16 | EMPIRE = 5, 17 | JEDI = 6 18 | } 19 | 20 | @GQLDUda( 21 | GQLDDescription("A character in the Star Wars Trilogy"), 22 | TypeKind.INTERFACE 23 | ) 24 | abstract class Character { 25 | @GQLDUda(GQLDDescription("The id of the character")) 26 | string id; 27 | 28 | @GQLDUda(GQLDDescription("The name of the character")) 29 | Nullable!string name; 30 | 31 | @GQLDUda( 32 | GQLDDescription("The friends of the character, or an empty list if " 33 | ~ "they have none." 34 | ) 35 | ) 36 | NullableStore!(Nullable!(Nullable!(Character)[])) friends; 37 | 38 | @GQLDUda(Ignore.yes) 39 | string[] friendsId; 40 | 41 | @GQLDUda(GQLDDescription("Which movies they appear in.")) 42 | Nullable!(Nullable!(Episode)[]) appearsIn; 43 | 44 | @GQLDUda( 45 | GQLDDescription("Where are they from and how they came to be who they " 46 | ~ " are.") 47 | ) 48 | Nullable!string secretBackstory; 49 | 50 | this(string id, string name, string[] friends, int[] appearsIn) { 51 | import std.array : array; 52 | import std.algorithm.iteration : map; 53 | this.id = id; 54 | this.name = name; 55 | this.friendsId = friends; 56 | this.appearsIn = appearsIn.map!(e => nullable(cast(Episode)e)).array; 57 | } 58 | } 59 | 60 | class Human : Character { 61 | @GQLDUda( 62 | GQLDDescription("The home planet of the human, or null if unknown.") 63 | ) 64 | string homePlanet; 65 | 66 | this(string id, string name, string[] friends, int[] appearsIn, 67 | string homePlanet) 68 | { 69 | super(id, name, friends, appearsIn); 70 | this.homePlanet = homePlanet; 71 | } 72 | } 73 | 74 | @GQLDUda( 75 | GQLDDescription("A mechanical creature in the Star Wars universe.") 76 | ) 77 | class Droid : Character { 78 | @GQLDUda( 79 | GQLDDescription("The primary function of the droid.") 80 | ) 81 | Nullable!string primaryFunction; 82 | 83 | this(string id, string name, string[] friends, int[] appearsIn, 84 | string primaryFunction) 85 | { 86 | super(id, name, friends, appearsIn); 87 | this.primaryFunction = nullable(primaryFunction); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/graphql/starwars/validation.d: -------------------------------------------------------------------------------- 1 | module graphql.starwars.validation; 2 | 3 | import std.typecons : Nullable, nullable; 4 | import std.format : format; 5 | import std.exception; 6 | import std.stdio; 7 | 8 | import vibe.data.json; 9 | 10 | import graphql.validation.exception; 11 | import graphql.constants; 12 | import graphql.parser; 13 | import graphql.builder; 14 | import graphql.lexer; 15 | import graphql.ast; 16 | import graphql.graphql; 17 | import graphql.helper; 18 | import graphql.starwars.data; 19 | import graphql.starwars.schema; 20 | import graphql.starwars.types; 21 | import graphql.validation.querybased; 22 | import graphql.validation.schemabased; 23 | 24 | @safe: 25 | 26 | void test(string s) { 27 | auto graphqld = new GraphQLD!(StarWarsSchema); 28 | auto l = Lexer(s); 29 | auto p = Parser(l); 30 | 31 | Document d = p.parseDocument(); 32 | const(Document) cd = d; 33 | QueryValidator fv = new QueryValidator(d); 34 | fv.accept(cd); 35 | noCylces(fv.fragmentChildren); 36 | allFragmentsReached(fv); 37 | SchemaValidator!StarWarsSchema sv = new SchemaValidator!StarWarsSchema(d, 38 | graphqld.schema 39 | ); 40 | sv.accept(cd); 41 | DefaultContext con; 42 | immutable Json gqld = graphqld.execute(d, Json.emptyObject(), con); 43 | } 44 | 45 | @safe unittest { 46 | assertNotThrown(test(` 47 | query NestedQueryWithFragment { 48 | hero { 49 | ...NameAndAppearances 50 | friends { 51 | ...NameAndAppearances 52 | friends { 53 | ...NameAndAppearances 54 | } 55 | } 56 | } 57 | } 58 | 59 | fragment NameAndAppearances on Character { 60 | name 61 | appearsIn 62 | }`)); 63 | } 64 | 65 | @safe unittest { 66 | assertThrown!FieldDoesNotExist(test(` 67 | query HeroSpaceshipQuery { 68 | hero { 69 | favoriteSpaceship 70 | } 71 | } 72 | `)); 73 | } 74 | 75 | @safe unittest { 76 | assertThrown!FieldDoesNotExist(test(` 77 | query HeroFieldsOnScalarQuery { 78 | hero { 79 | name { 80 | firstCharacterOfName 81 | } 82 | } 83 | } 84 | `)); 85 | } 86 | 87 | @safe unittest { 88 | assertNotThrown(test(` 89 | query DroidFieldInFragment { 90 | hero { 91 | name 92 | ...DroidFields 93 | } 94 | } 95 | 96 | fragment DroidFields on Droid { 97 | primaryFunction 98 | } 99 | `)); 100 | } 101 | 102 | @safe unittest { 103 | assertNotThrown(test(` 104 | query DroidFieldInFragment { 105 | hero { 106 | name 107 | ... on Droid { 108 | primaryFunction 109 | } 110 | } 111 | } 112 | `)); 113 | } 114 | -------------------------------------------------------------------------------- /source/graphql/testschema.d: -------------------------------------------------------------------------------- 1 | module graphql.testschema; 2 | 3 | import std.algorithm.iteration : map; 4 | import std.datetime : DateTime, Date; 5 | import std.typecons : Nullable; 6 | import std.format : format; 7 | 8 | import vibe.data.json; 9 | import nullablestore; 10 | 11 | import graphql.schema.directives; 12 | import graphql.helper; 13 | 14 | import graphql.uda; 15 | 16 | @safe: 17 | 18 | Json datetimeToJson(DateTime dt) { 19 | return Json(dt.toISOExtString()); 20 | } 21 | 22 | // The Schema used by graphqld 23 | 24 | union SearchResult { 25 | Android android; 26 | Humanoid humanoid; 27 | Starship ship; 28 | } 29 | 30 | string dtToString(DateTime dt) { 31 | return dt.toISOExtString(); 32 | } 33 | 34 | DateTime stringToDT(string s) { 35 | return DateTime.fromISOExtString(s); 36 | } 37 | 38 | struct Input { 39 | size_t first; 40 | @optional 41 | Nullable!string after; 42 | } 43 | 44 | @GQLDUda(TypeKind.OBJECT) 45 | struct Query { 46 | @GQLDUda( 47 | GQLDDescription("Get the captain by Series") 48 | ) 49 | Character captain(Series series); 50 | @GQLDUda( 51 | GQLDDeprecated(IsDeprecated.yes, "To complex") 52 | ) 53 | SearchResult search(string name); 54 | Nullable!Starship starship(long id); 55 | Nullable!StarshipSimple starshipSimple(long id); 56 | Nullable!StarshipSimple2 starshipSimple2(long id); 57 | Nullable!StarshipSimple3 starshipSimple3(long id); 58 | Starship starshipDoesNotExist(); 59 | Starship[] starships(float overSize = 100.0); 60 | Starship[] shipsselection(long[] ids); 61 | Nullable!Character character(long id); 62 | Character[] characters(Series series); 63 | Humanoid[] humanoids(); 64 | Android[] androids(); 65 | Android[] resolverWillThrow(); 66 | GQLDCustomLeaf!(DateTime, dtToString, stringToDT) currentTime(); 67 | int currentTime(); 68 | Starship numberBetween(Input searchInput); 69 | 70 | NullableStore!(Starship[]) alwaysEmpty(); 71 | 72 | @GQLDUda(Ignore.yes) 73 | void ignoreMe() { 74 | } 75 | } 76 | 77 | unittest { 78 | Query d; 79 | } 80 | 81 | @GQLDUda(TypeKind.INPUT_OBJECT) struct Vector { float x, y; } 82 | @GQLDUda(TypeKind.INPUT_OBJECT) 83 | struct AddCrewmanData { 84 | string name; 85 | long shipId; 86 | Vector location; 87 | Series[] series; 88 | } 89 | 90 | /*@GQLDUda(TypeKind.INPUT_OBJECT) 91 | struct AddCrewmanComplexInput { 92 | Character crewman; 93 | Starship starship; 94 | }*/ 95 | 96 | interface Mutation { 97 | Character addCrewman(AddCrewmanData input); 98 | Character getStupidestCrewman(); 99 | //Starship addCrewmanCompley(AddCrewmanComplexInput input); 100 | } 101 | 102 | interface Subscription { 103 | Starship[] starships(); 104 | } 105 | 106 | class Schema { 107 | Query queryType; 108 | Mutation mutationType; 109 | Subscription subscriptionType; 110 | DefaultDirectives directives; 111 | } 112 | 113 | enum Series { 114 | TheOriginalSeries, 115 | TheNextGeneration, 116 | DeepSpaceNine, 117 | Voyager, 118 | Enterprise, 119 | Discovery 120 | } 121 | 122 | @GQLDUda(TypeKind.INTERFACE) 123 | abstract class Character { 124 | long id; 125 | string name; 126 | Series[] series; 127 | Character[] commands; 128 | Nullable!Starship ship; 129 | NullableStore!Starship ships; 130 | Character[] commanders; 131 | Nullable!Starship allwaysNull; 132 | Nullable!int alsoAllwaysNull; 133 | const bool isDead; 134 | @GQLDUda( 135 | GQLDDeprecated(IsDeprecated.yes, "Stupid name") 136 | ) 137 | int someOldField; 138 | 139 | //NullableStore!AddCrewmanData data; 140 | } 141 | 142 | class Humanoid : Character { 143 | string species; 144 | GQLDCustomLeaf!(Date, dToString, stringToDT) dateOfBirth; 145 | } 146 | 147 | class Android : Character { 148 | string primaryFunction; 149 | 150 | @GQLDUda(Ignore.yes) 151 | void ignoreMeToo() { 152 | } 153 | } 154 | 155 | @GQLDUda( 156 | GQLDDescription("The thing Chracters fly around in") 157 | ) 158 | class Starship { 159 | long id; 160 | string name; 161 | 162 | @GQLDUda( 163 | GQLDDescription("The name used when speaking about the ship") 164 | ) 165 | string designation; 166 | double size; 167 | 168 | @GQLDUda( 169 | GQLDDescription("The person in charge") 170 | ) 171 | Character commander; 172 | Nullable!(Series)[] series; 173 | Character[] crew; 174 | 175 | this(long id, string designation, double size, string name) { 176 | this.id = id; 177 | this.designation = designation; 178 | this.size = size; 179 | this.name = name; 180 | } 181 | 182 | override string toString() const @safe { 183 | return format("Ship(id(%d), designation(%s), size(%.2f), name(%s)" 184 | ~ "commander(%s), series[%(%s,%)], crew[%(%s,%)])", 185 | id, designation, size, name, commander.name, series, 186 | crew.map!(a => a.name) 187 | ); 188 | } 189 | } 190 | 191 | class StarshipSimple { 192 | //Nullable!(Series)[] series; 193 | //long id; 194 | Character commander; 195 | 196 | override string toString() const @safe { 197 | //return format("ShipSimple(series[%(%s,%)])", series); 198 | return ""; 199 | } 200 | } 201 | 202 | class StarshipSimple2 { 203 | //Nullable!(Series)[] series; 204 | long id; 205 | 206 | override string toString() const @safe { 207 | //return format("ShipSimple(series[%(%s,%)])", series); 208 | return ""; 209 | } 210 | } 211 | 212 | class StarshipSimple3 { 213 | Nullable!(Series)[] series; 214 | 215 | override string toString() const @safe { 216 | //return format("ShipSimple(series[%(%s,%)])", series); 217 | return ""; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /source/graphql/tokenmodule.d: -------------------------------------------------------------------------------- 1 | module graphql.tokenmodule; 2 | 3 | import graphql.visitor; 4 | 5 | enum TokenType { 6 | undefined, 7 | exclamation, 8 | dollar, 9 | lparen, 10 | rparen, 11 | dots, 12 | colon, 13 | equal, 14 | at, 15 | lbrack, 16 | rbrack, 17 | lcurly, 18 | rcurly, 19 | pipe, 20 | name, 21 | intValue, 22 | floatValue, 23 | stringValue, 24 | query, 25 | mutation, 26 | subscription, 27 | fragment, 28 | on_, 29 | // alias_, 30 | true_, 31 | false_, 32 | null_, 33 | comment, 34 | comma, 35 | union_, 36 | type, 37 | typename, 38 | // skip, 39 | // include_, 40 | input, 41 | scalar, 42 | schema, 43 | // schema__, 44 | directive, 45 | enum_, 46 | interface_, 47 | implements, 48 | extend, 49 | } 50 | 51 | static immutable string[cast(TokenType)(TokenType.max + 1)] tokenStrings = [ 52 | null, 53 | "!", 54 | "$", 55 | "(", 56 | ")", 57 | "...", 58 | ":", 59 | "=", 60 | "@", 61 | "[", 62 | "]", 63 | "{", 64 | "}", 65 | "|", 66 | null, 67 | null, 68 | null, 69 | null, 70 | "query", 71 | "mutation", 72 | "subscription", 73 | "fragment", 74 | "on", 75 | // "alias", 76 | "true", 77 | "false", 78 | "null", 79 | null, 80 | ",", 81 | "union", 82 | "type", 83 | "__typename", 84 | // "skip", 85 | // "include", 86 | "input", 87 | "scalar", 88 | "schema", 89 | // "__schema", 90 | "directive", 91 | "enum", 92 | "interface", 93 | "implements", 94 | "extend", 95 | ]; 96 | 97 | struct Token { 98 | @safe: 99 | string value; 100 | uint line; 101 | ushort column; 102 | 103 | TokenType type; 104 | 105 | this(TokenType type) { 106 | this.type = type; 107 | this.value = tokenStrings[type]; 108 | } 109 | 110 | this(TokenType type, size_t line, size_t column) { 111 | this(type); 112 | this.line = cast(uint)line; 113 | this.column = cast(ushort)column; 114 | } 115 | 116 | this(TokenType type, string value) { 117 | this(type); 118 | this.value = value; 119 | } 120 | 121 | this(TokenType type, string value, size_t line, size_t column) { 122 | this(type, line, column); 123 | this.value = value; 124 | } 125 | 126 | void visit(ConstVisitor vis) { 127 | } 128 | 129 | void visit(ConstVisitor vis) const { 130 | } 131 | 132 | void visit(Visitor vis) { 133 | } 134 | 135 | void visit(Visitor vis) const { 136 | } 137 | 138 | string toString() const { 139 | import std.format : format; 140 | return format("Token(line: %s, col: %s, type: '%s' , value: '%s')", this.line, this.column, this.type, 141 | this.value); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /source/graphql/treevisitor.d: -------------------------------------------------------------------------------- 1 | module graphql.treevisitor; 2 | 3 | import std.traits : Unqual; 4 | import graphql.ast; 5 | import graphql.visitor; 6 | import graphql.tokenmodule; 7 | 8 | class TreeVisitor : ConstVisitor { 9 | @safe : 10 | 11 | import std.stdio : write, writeln; 12 | 13 | alias accept = ConstVisitor.accept; 14 | 15 | int depth; 16 | 17 | this(int d) { 18 | this.depth = d; 19 | } 20 | 21 | void genIndent() { 22 | foreach(i; 0 .. this.depth) { 23 | write(" "); 24 | } 25 | } 26 | 27 | override void accept(const(Document) obj) { 28 | this.genIndent(); 29 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 30 | ++this.depth; 31 | super.accept(obj); 32 | --this.depth; 33 | } 34 | 35 | override void accept(const(Definitions) obj) { 36 | this.genIndent(); 37 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 38 | ++this.depth; 39 | super.accept(obj); 40 | --this.depth; 41 | } 42 | 43 | override void accept(const(Definition) obj) { 44 | this.genIndent(); 45 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 46 | ++this.depth; 47 | super.accept(obj); 48 | --this.depth; 49 | } 50 | 51 | override void accept(const(OperationDefinition) obj) { 52 | this.genIndent(); 53 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 54 | ++this.depth; 55 | super.accept(obj); 56 | --this.depth; 57 | } 58 | 59 | override void accept(const(SelectionSet) obj) { 60 | this.genIndent(); 61 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 62 | ++this.depth; 63 | super.accept(obj); 64 | --this.depth; 65 | } 66 | 67 | override void accept(const(OperationType) obj) { 68 | this.genIndent(); 69 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 70 | ++this.depth; 71 | super.accept(obj); 72 | --this.depth; 73 | } 74 | 75 | override void accept(const(Selections) obj) { 76 | this.genIndent(); 77 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 78 | ++this.depth; 79 | super.accept(obj); 80 | --this.depth; 81 | } 82 | 83 | override void accept(const(Selection) obj) { 84 | this.genIndent(); 85 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 86 | ++this.depth; 87 | super.accept(obj); 88 | --this.depth; 89 | } 90 | 91 | override void accept(const(FragmentSpread) obj) { 92 | this.genIndent(); 93 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 94 | ++this.depth; 95 | super.accept(obj); 96 | --this.depth; 97 | } 98 | 99 | override void accept(const(InlineFragment) obj) { 100 | this.genIndent(); 101 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 102 | ++this.depth; 103 | super.accept(obj); 104 | --this.depth; 105 | } 106 | 107 | override void accept(const(Field) obj) { 108 | this.genIndent(); 109 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 110 | ++this.depth; 111 | super.accept(obj); 112 | --this.depth; 113 | } 114 | 115 | override void accept(const(FieldName) obj) { 116 | this.genIndent(); 117 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 118 | ++this.depth; 119 | super.accept(obj); 120 | --this.depth; 121 | } 122 | 123 | override void accept(const(Identifier) obj) { 124 | this.genIndent(); 125 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 126 | ++this.depth; 127 | super.accept(obj); 128 | --this.depth; 129 | } 130 | 131 | override void accept(const(Arguments) obj) { 132 | this.genIndent(); 133 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 134 | ++this.depth; 135 | super.accept(obj); 136 | --this.depth; 137 | } 138 | 139 | override void accept(const(ArgumentList) obj) { 140 | this.genIndent(); 141 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 142 | ++this.depth; 143 | super.accept(obj); 144 | --this.depth; 145 | } 146 | 147 | override void accept(const(Argument) obj) { 148 | this.genIndent(); 149 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 150 | ++this.depth; 151 | super.accept(obj); 152 | --this.depth; 153 | } 154 | 155 | override void accept(const(FragmentDefinition) obj) { 156 | this.genIndent(); 157 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 158 | ++this.depth; 159 | super.accept(obj); 160 | --this.depth; 161 | } 162 | 163 | override void accept(const(Directives) obj) { 164 | this.genIndent(); 165 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 166 | ++this.depth; 167 | super.accept(obj); 168 | --this.depth; 169 | } 170 | 171 | override void accept(const(Directive) obj) { 172 | this.genIndent(); 173 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 174 | ++this.depth; 175 | super.accept(obj); 176 | --this.depth; 177 | } 178 | 179 | override void accept(const(VariableDefinitions) obj) { 180 | this.genIndent(); 181 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 182 | ++this.depth; 183 | super.accept(obj); 184 | --this.depth; 185 | } 186 | 187 | override void accept(const(VariableDefinitionList) obj) { 188 | this.genIndent(); 189 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 190 | ++this.depth; 191 | super.accept(obj); 192 | --this.depth; 193 | } 194 | 195 | override void accept(const(VariableDefinition) obj) { 196 | this.genIndent(); 197 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 198 | ++this.depth; 199 | super.accept(obj); 200 | --this.depth; 201 | } 202 | 203 | override void accept(const(Variable) obj) { 204 | this.genIndent(); 205 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 206 | ++this.depth; 207 | super.accept(obj); 208 | --this.depth; 209 | } 210 | 211 | override void accept(const(DefaultValue) obj) { 212 | this.genIndent(); 213 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 214 | ++this.depth; 215 | super.accept(obj); 216 | --this.depth; 217 | } 218 | 219 | override void accept(const(ValueOrVariable) obj) { 220 | this.genIndent(); 221 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 222 | ++this.depth; 223 | super.accept(obj); 224 | --this.depth; 225 | } 226 | 227 | override void accept(const(Value) obj) { 228 | this.genIndent(); 229 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 230 | ++this.depth; 231 | super.accept(obj); 232 | --this.depth; 233 | } 234 | 235 | override void accept(const(Type) obj) { 236 | this.genIndent(); 237 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 238 | ++this.depth; 239 | super.accept(obj); 240 | --this.depth; 241 | } 242 | 243 | override void accept(const(ListType) obj) { 244 | this.genIndent(); 245 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 246 | ++this.depth; 247 | super.accept(obj); 248 | --this.depth; 249 | } 250 | 251 | override void accept(const(Values) obj) { 252 | this.genIndent(); 253 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 254 | ++this.depth; 255 | super.accept(obj); 256 | --this.depth; 257 | } 258 | 259 | override void accept(const(Array) obj) { 260 | this.genIndent(); 261 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 262 | ++this.depth; 263 | super.accept(obj); 264 | --this.depth; 265 | } 266 | 267 | override void accept(const(ObjectValues) obj) { 268 | this.genIndent(); 269 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 270 | ++this.depth; 271 | super.accept(obj); 272 | --this.depth; 273 | } 274 | 275 | override void accept(const(ObjectType) obj) { 276 | this.genIndent(); 277 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 278 | ++this.depth; 279 | super.accept(obj); 280 | --this.depth; 281 | } 282 | 283 | override void accept(const(TypeSystemDefinition) obj) { 284 | this.genIndent(); 285 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 286 | ++this.depth; 287 | super.accept(obj); 288 | --this.depth; 289 | } 290 | 291 | override void accept(const(TypeDefinition) obj) { 292 | this.genIndent(); 293 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 294 | ++this.depth; 295 | super.accept(obj); 296 | --this.depth; 297 | } 298 | 299 | override void accept(const(SchemaDefinition) obj) { 300 | this.genIndent(); 301 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 302 | ++this.depth; 303 | super.accept(obj); 304 | --this.depth; 305 | } 306 | 307 | override void accept(const(OperationTypeDefinitions) obj) { 308 | this.genIndent(); 309 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 310 | ++this.depth; 311 | super.accept(obj); 312 | --this.depth; 313 | } 314 | 315 | override void accept(const(OperationTypeDefinition) obj) { 316 | this.genIndent(); 317 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 318 | ++this.depth; 319 | super.accept(obj); 320 | --this.depth; 321 | } 322 | 323 | override void accept(const(ScalarTypeDefinition) obj) { 324 | this.genIndent(); 325 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 326 | ++this.depth; 327 | super.accept(obj); 328 | --this.depth; 329 | } 330 | 331 | override void accept(const(ObjectTypeDefinition) obj) { 332 | this.genIndent(); 333 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 334 | ++this.depth; 335 | super.accept(obj); 336 | --this.depth; 337 | } 338 | 339 | override void accept(const(FieldDefinitions) obj) { 340 | this.genIndent(); 341 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 342 | ++this.depth; 343 | super.accept(obj); 344 | --this.depth; 345 | } 346 | 347 | override void accept(const(FieldDefinition) obj) { 348 | this.genIndent(); 349 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 350 | ++this.depth; 351 | super.accept(obj); 352 | --this.depth; 353 | } 354 | 355 | override void accept(const(ImplementsInterfaces) obj) { 356 | this.genIndent(); 357 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 358 | ++this.depth; 359 | super.accept(obj); 360 | --this.depth; 361 | } 362 | 363 | override void accept(const(NamedTypes) obj) { 364 | this.genIndent(); 365 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 366 | ++this.depth; 367 | super.accept(obj); 368 | --this.depth; 369 | } 370 | 371 | override void accept(const(ArgumentsDefinition) obj) { 372 | this.genIndent(); 373 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 374 | ++this.depth; 375 | super.accept(obj); 376 | --this.depth; 377 | } 378 | 379 | override void accept(const(InputValueDefinitions) obj) { 380 | this.genIndent(); 381 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 382 | ++this.depth; 383 | super.accept(obj); 384 | --this.depth; 385 | } 386 | 387 | override void accept(const(InputValueDefinition) obj) { 388 | this.genIndent(); 389 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 390 | ++this.depth; 391 | super.accept(obj); 392 | --this.depth; 393 | } 394 | 395 | override void accept(const(InterfaceTypeDefinition) obj) { 396 | this.genIndent(); 397 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 398 | ++this.depth; 399 | super.accept(obj); 400 | --this.depth; 401 | } 402 | 403 | override void accept(const(UnionTypeDefinition) obj) { 404 | this.genIndent(); 405 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 406 | ++this.depth; 407 | super.accept(obj); 408 | --this.depth; 409 | } 410 | 411 | override void accept(const(UnionMembers) obj) { 412 | this.genIndent(); 413 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 414 | ++this.depth; 415 | super.accept(obj); 416 | --this.depth; 417 | } 418 | 419 | override void accept(const(EnumTypeDefinition) obj) { 420 | this.genIndent(); 421 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 422 | ++this.depth; 423 | super.accept(obj); 424 | --this.depth; 425 | } 426 | 427 | override void accept(const(EnumValueDefinitions) obj) { 428 | this.genIndent(); 429 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 430 | ++this.depth; 431 | super.accept(obj); 432 | --this.depth; 433 | } 434 | 435 | override void accept(const(EnumValueDefinition) obj) { 436 | this.genIndent(); 437 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 438 | ++this.depth; 439 | super.accept(obj); 440 | --this.depth; 441 | } 442 | 443 | override void accept(const(TypeExtensionDefinition) obj) { 444 | this.genIndent(); 445 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 446 | ++this.depth; 447 | super.accept(obj); 448 | --this.depth; 449 | } 450 | 451 | override void accept(const(DirectiveDefinition) obj) { 452 | this.genIndent(); 453 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 454 | ++this.depth; 455 | super.accept(obj); 456 | --this.depth; 457 | } 458 | 459 | override void accept(const(DirectiveLocations) obj) { 460 | this.genIndent(); 461 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 462 | ++this.depth; 463 | super.accept(obj); 464 | --this.depth; 465 | } 466 | 467 | override void accept(const(InputObjectTypeDefinition) obj) { 468 | this.genIndent(); 469 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 470 | ++this.depth; 471 | super.accept(obj); 472 | --this.depth; 473 | } 474 | 475 | override void accept(const(Description) obj) { 476 | this.genIndent(); 477 | writeln(Unqual!(typeof(obj)).stringof,":", obj.ruleSelection); 478 | ++this.depth; 479 | super.accept(obj); 480 | --this.depth; 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /source/graphql/uda.d: -------------------------------------------------------------------------------- 1 | module graphql.uda; 2 | 3 | import std.array : empty; 4 | import std.datetime : DateTime; 5 | import std.traits : isBuiltinType; 6 | import std.meta : Filter; 7 | 8 | @safe: 9 | 10 | enum TypeKind { 11 | UNDEFINED, 12 | SCALAR, 13 | OBJECT, 14 | INTERFACE, 15 | UNION, 16 | ENUM, 17 | INPUT_OBJECT, 18 | LIST, 19 | NON_NULL 20 | } 21 | 22 | struct GQLDUdaData { 23 | TypeKind typeKind; 24 | GQLDDeprecatedData deprecationInfo; 25 | GQLDDescription description; 26 | Ignore ignore; 27 | IgnoreForInput ignoreForInput; 28 | RequiredForInput requiredForInput; 29 | } 30 | 31 | enum IgnoreForInput { 32 | undefined, 33 | yes, 34 | no 35 | } 36 | 37 | enum RequiredForInput { 38 | undefined, 39 | yes, 40 | no 41 | } 42 | 43 | enum Ignore { 44 | undefined, 45 | yes, 46 | no 47 | } 48 | 49 | enum IsDeprecated { 50 | undefined, 51 | yes, 52 | no 53 | } 54 | 55 | string toStringImpl(T)(T t) { 56 | static if(__traits(hasMember, T, "toString")) { 57 | return t.toString(); 58 | } else { 59 | import std.format : format; 60 | return format("%s", t); 61 | } 62 | } 63 | 64 | /* The wrapped 65 | T = the wrapped type 66 | SerializationFun = the function to use to serialize T 67 | */ 68 | struct GQLDCustomLeaf(T, alias SerializationFun, alias DeserializationFun) { 69 | alias Type = T; 70 | Type value; 71 | alias value this; 72 | 73 | alias Fun = SerializationFun; 74 | alias DeFun = SerializationFun; 75 | 76 | this(Type value) { 77 | this.value = value; 78 | } 79 | 80 | void opAssign(Type value) { 81 | this.value = value; 82 | } 83 | 84 | GQLDCustomLeaf!(T, SerializationFun, DeserializationFun) _from(T input) @safe { 85 | return GQLDCustomLeaf!(T, SerializationFun, DeserializationFun)(input); 86 | } 87 | 88 | static auto fromRepresentation(T input) @safe { 89 | auto myself = typeof(this).init; 90 | return myself._from(input); 91 | } 92 | 93 | T toRepresentation() @safe const { 94 | return this.value; 95 | } 96 | 97 | } 98 | 99 | private string tS(DateTime dt) { 100 | return dt.toISOExtString(); 101 | } 102 | 103 | private DateTime fS(string s) { 104 | return DateTime.fromISOExtString(s); 105 | } 106 | 107 | unittest { 108 | import vibe.data.json; 109 | 110 | Json fun(DateTime dt) { 111 | return Json(dt.toISOExtString()); 112 | } 113 | 114 | auto f = GQLDCustomLeaf!(DateTime, tS, fS)(); 115 | 116 | GQLDCustomLeaf!(DateTime, tS, fS) dt = DateTime(1337, 1, 1); 117 | } 118 | 119 | unittest { 120 | import std.typecons : Nullable, nullable; 121 | import std.datetime : DateTime; 122 | Nullable!(GQLDCustomLeaf!(DateTime, tS, fS)) dt; 123 | } 124 | 125 | struct GQLDDeprecatedData { 126 | IsDeprecated isDeprecated; 127 | string deprecationReason; 128 | } 129 | 130 | struct GQLDDescription { 131 | string text; 132 | string getText() const { 133 | return text !is null && !text.empty ? this.text : ""; 134 | } 135 | } 136 | 137 | GQLDDeprecatedData GQLDDeprecated(IsDeprecated isDeprecated) { 138 | return GQLDDeprecatedData(isDeprecated, ""); 139 | } 140 | 141 | GQLDDeprecatedData GQLDDeprecated(IsDeprecated isDeprecated, 142 | string deprecationReason) 143 | { 144 | return GQLDDeprecatedData(isDeprecated, deprecationReason); 145 | } 146 | 147 | GQLDUdaData GQLDUda(Args...)(Args args) { 148 | GQLDUdaData ret; 149 | static foreach(mem; __traits(allMembers, GQLDUdaData)) { 150 | static foreach(arg; args) { 151 | static if(is(typeof(__traits(getMember, ret, mem)) == 152 | typeof(arg))) 153 | { 154 | __traits(getMember, ret, mem) = arg; 155 | } 156 | } 157 | } 158 | return ret; 159 | } 160 | 161 | private template isGQLDUdaData(alias Type) { 162 | enum isGQLDUdaData = is(typeof(Type) == GQLDUdaData); 163 | } 164 | 165 | private template getGQLDUdaData(Type, string mem) { 166 | alias getGQLDUdaData = 167 | Filter!(isGQLDUdaData, 168 | __traits(getAttributes, __traits(getMember, Type, mem))); 169 | } 170 | 171 | private template getGQLDUdaData(Type) { 172 | alias getGQLDUdaData = 173 | Filter!(isGQLDUdaData, __traits(getAttributes, Type)); 174 | } 175 | 176 | template filterGQLDUdaParameter(Attrs...) { 177 | alias filterGQLDUdaParameter = Filter!(isGQLDUdaData, Attrs); 178 | } 179 | 180 | template getUdaData(Type) { 181 | static if(isBuiltinType!Type) { 182 | enum getUdaData = GQLDUdaData.init; 183 | } else { 184 | alias GQLDUdaDataAS = getGQLDUdaData!Type; 185 | static if(GQLDUdaDataAS.length > 0) { 186 | enum getUdaData = GQLDUdaDataAS[0]; 187 | } else { 188 | enum getUdaData = GQLDUdaData.init; 189 | } 190 | } 191 | } 192 | 193 | template getUdaData(Type, string mem) { 194 | alias GQLDUdaDataAS = getGQLDUdaData!(Type, mem); 195 | static if(GQLDUdaDataAS.length > 0) { 196 | enum getUdaData = GQLDUdaDataAS[0]; 197 | } else { 198 | enum getUdaData = GQLDUdaData.init; 199 | } 200 | } 201 | 202 | unittest { 203 | @GQLDUda( 204 | GQLDDeprecated(IsDeprecated.yes, "Stupid name"), 205 | GQLDDescription("You normal test struct") 206 | ) 207 | struct Foo { 208 | @GQLDUda( 209 | GQLDDeprecated(IsDeprecated.no, "Very good name"), 210 | GQLDDescription("Contains something") 211 | ) 212 | int bar; 213 | 214 | int args; 215 | } 216 | 217 | enum GQLDUdaData fd = getUdaData!Foo; 218 | static assert(fd.deprecationInfo.isDeprecated == IsDeprecated.yes); 219 | static assert(fd.deprecationInfo.deprecationReason == "Stupid name"); 220 | static assert(fd.description.text == "You normal test struct"); 221 | 222 | enum GQLDUdaData bd = getUdaData!(Foo, "bar"); 223 | static assert(bd.deprecationInfo.isDeprecated == IsDeprecated.no); 224 | static assert(bd.deprecationInfo.deprecationReason == "Very good name"); 225 | static assert(bd.description.text == "Contains something"); 226 | 227 | enum GQLDUdaData ad = getUdaData!(Foo, "args"); 228 | static assert(ad.deprecationInfo.isDeprecated == IsDeprecated.undefined); 229 | static assert(ad.deprecationInfo.deprecationReason == ""); 230 | static assert(ad.description.text == ""); 231 | } 232 | -------------------------------------------------------------------------------- /source/graphql/validation/exception.d: -------------------------------------------------------------------------------- 1 | module graphql.validation.exception; 2 | 3 | @safe: 4 | 5 | class ValidationException : Exception { 6 | this(string msg, string f, size_t l) { 7 | super(msg, f, l); 8 | } 9 | } 10 | 11 | class FragmentNotFoundException : ValidationException { 12 | this(string msg, string f, size_t l) { 13 | super(msg, f, l); 14 | } 15 | } 16 | 17 | class FragmentCycleException : ValidationException { 18 | this(string msg, string f, size_t l) { 19 | super(msg, f, l); 20 | } 21 | } 22 | 23 | class FragmentNameAlreadyInUseException : ValidationException { 24 | this(string msg, string f, size_t l) { 25 | super(msg, f, l); 26 | } 27 | } 28 | 29 | class UnusedFragmentsException : ValidationException { 30 | this(string msg, string f, size_t l) { 31 | super(msg, f, l); 32 | } 33 | } 34 | 35 | class LoneAnonymousOperationException : ValidationException { 36 | this(string msg, string f, size_t l) { 37 | super(msg, f, l); 38 | } 39 | } 40 | 41 | class NonUniqueOperationNameException : ValidationException { 42 | this(string msg, string f, size_t l) { 43 | super(msg, f, l); 44 | } 45 | } 46 | 47 | class NoTypeSystemDefinitionException : ValidationException { 48 | this(string msg, string f, size_t l) { 49 | super(msg, f, l); 50 | } 51 | } 52 | 53 | class ArgumentsNotUniqueException : ValidationException { 54 | this(string msg, string f, size_t l) { 55 | super(msg, f, l); 56 | } 57 | } 58 | 59 | class VariablesNotUniqueException : ValidationException { 60 | this(string msg, string f, size_t l) { 61 | super(msg, f, l); 62 | } 63 | } 64 | 65 | class VariablesUseException : ValidationException { 66 | this(string msg, string f, size_t l) { 67 | super(msg, f, l); 68 | } 69 | } 70 | 71 | class SingleRootField : ValidationException { 72 | this(string msg, string f, size_t l) { 73 | super(msg, f, l); 74 | } 75 | } 76 | 77 | class FieldDoesNotExist : ValidationException { 78 | this(string msg, string f, size_t l) { 79 | super(msg, f, l); 80 | } 81 | } 82 | 83 | class UnknownTypeName : ValidationException { 84 | this(string msg, string f, size_t l) { 85 | super(msg, f, l); 86 | } 87 | } 88 | 89 | class FragmentNotOnCompositeType : ValidationException { 90 | this(string msg, string f, size_t l) { 91 | super(msg, f, l); 92 | } 93 | } 94 | 95 | class LeafIsNotAScalar : ValidationException { 96 | this(string msg, string f, size_t l) { 97 | super(msg, f, l); 98 | } 99 | } 100 | 101 | class ContradictingDirectives : ValidationException { 102 | this(string msg, string f, size_t l) { 103 | super(msg, f, l); 104 | } 105 | } 106 | 107 | class DirectiveNotUnique : ValidationException { 108 | this(string msg, string f, size_t l) { 109 | super(msg, f, l); 110 | } 111 | } 112 | 113 | class VariableInputTypeMismatch : ValidationException { 114 | this(string msg, string f, size_t l) { 115 | super(msg, f, l); 116 | } 117 | } 118 | 119 | class ArgumentDoesNotExist : ValidationException { 120 | this(string msg, string f, size_t l) { 121 | super(msg, f, l); 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | An example how to use graphqld with vibe.d 2 | 3 | 1. dub 4 | 2. point graphiql-app to localhost:8080 5 | 3. 6 | 4. 7 | 8 | [graphqi-all](https://github.com/skevy/graphiql-app) 9 | -------------------------------------------------------------------------------- /test/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "burner" 4 | ], 5 | "copyright": "Copyright © 2019, burner", 6 | "dependencies": { 7 | "graphqld": { "path" : "../" }, 8 | "nullablestore": "~>2.1.0", 9 | "vibe-d": ">=0.9.0" 10 | }, 11 | "dflags": [ "-d"], 12 | "dflags-ldc": ["-d", "-ftime-trace", "-ftime-trace-file=$PACKAGE_DIR/trace.json"], 13 | "versions" : ["VibeNoSSL"], 14 | "description": "A simple vibe.d server application.", 15 | "license": "GPL3", 16 | "name": "test", 17 | "subConfigurations": { 18 | "vibe-d:tls": "notls" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "diet-ng": "1.8.2", 5 | "eventcore": "0.9.35", 6 | "exceptionhandling": "1.0.0", 7 | "fixedsizearray": "1.3.0", 8 | "graphqld": {"path":"../"}, 9 | "mir-linux-kernel": "1.2.1", 10 | "nullablestore": "2.1.0", 11 | "openssl": "3.3.4", 12 | "openssl-static": "1.0.5+3.0.8", 13 | "stdx-allocator": "2.77.5", 14 | "taggedalgebraic": "0.11.23", 15 | "vibe-container": "1.3.1", 16 | "vibe-core": "2.9.5", 17 | "vibe-d": "0.10.1", 18 | "vibe-http": "1.1.2", 19 | "vibe-inet": "1.0.1", 20 | "vibe-serialization": "1.0.5", 21 | "vibe-stream": "1.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/introspectionquery.gql: -------------------------------------------------------------------------------- 1 | { 2 | "query": " 3 | query IntrospectionQuery { 4 | __schema { 5 | queryType { name } 6 | mutationType { name } 7 | subscriptionType { name } 8 | types { 9 | ...FullType 10 | } 11 | directives { 12 | name 13 | description 14 | locations 15 | args { 16 | ...InputValue 17 | } 18 | } 19 | } 20 | } 21 | 22 | fragment FullType on __Type { 23 | kind 24 | name 25 | description 26 | fields(includeDeprecated: true) { 27 | name 28 | description 29 | args { 30 | ...InputValue 31 | } 32 | type { 33 | ...TypeRef 34 | } 35 | isDeprecated 36 | deprecationReason 37 | } 38 | inputFields { 39 | ...InputValue 40 | } 41 | interfaces { 42 | ...TypeRef 43 | } 44 | enumValues(includeDeprecated: true) { 45 | name 46 | description 47 | isDeprecated 48 | deprecationReason 49 | } 50 | possibleTypes { 51 | ...TypeRef 52 | } 53 | } 54 | 55 | fragment InputValue on __InputValue { 56 | name 57 | description 58 | type { ...TypeRef } 59 | defaultValue 60 | } 61 | 62 | fragment TypeRef on __Type { 63 | kind 64 | name 65 | ofType { 66 | kind 67 | name 68 | ofType { 69 | kind 70 | name 71 | ofType { 72 | kind 73 | name 74 | ofType { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | " 95 | } 96 | -------------------------------------------------------------------------------- /test/schema.gql: -------------------------------------------------------------------------------- 1 | schema { 2 | mutation: mutationType 3 | query: queryType 4 | subscription: subscriptionType 5 | } 6 | type mutationType { 7 | getStupidestCrewman: Character! 8 | addCrewman(input: AddCrewmanData!): Character! 9 | name: String! 10 | } 11 | type queryType { 12 | shipsselection(ids: [Int!]!): [Starship!]! 13 | characters(series: Series!): [Character!]! 14 | starships(overSize: Float!): [Starship!]! 15 | captain(series: Series!): Character! 16 | humanoids: [Humanoid!]! 17 | currentTime: DateTime! 18 | starship(id: Int!): Starship 19 | starshipSimple2(id: Int!): StarshipSimple2 20 | name: String! 21 | androids: [Android!]! 22 | resolverWillThrow: [Android!]! 23 | numberBetween(searchInput: InputIn!): Starship! 24 | search(name: String!): SearchResult! @deprecated(reason: "To complex") 25 | starshipSimple3(id: Int!): StarshipSimple3 26 | starshipSimple(id: Int!): StarshipSimple 27 | starshipDoesNotExist: Starship! 28 | character(id: Int!): Character 29 | alwaysEmpty: [Starship!] 30 | } 31 | type StarshipSimple { 32 | commander: Character! 33 | } 34 | type subscriptionType { 35 | starships: [Starship!]! 36 | name: String! 37 | } 38 | type Android implements Character { 39 | isDead: Boolean! 40 | ships: Starship 41 | series: [Series!]! 42 | id: Int! 43 | ship: Starship 44 | primaryFunction: String! 45 | name: String! 46 | allwaysNull: Starship 47 | commands: [Character!]! 48 | alsoAllwaysNull: Int 49 | someOldField: Int! @deprecated(reason: "Stupid name") 50 | commanders: [Character!]! 51 | } 52 | enum Series { 53 | TheOriginalSeries, 54 | TheNextGeneration, 55 | DeepSpaceNine, 56 | Voyager, 57 | Enterprise, 58 | Discovery, 59 | } 60 | type Input { 61 | first: Int! 62 | after: String 63 | } 64 | input InputIn { 65 | first: Int! 66 | after: String 67 | } 68 | type Starship { 69 | series: [Series]! 70 | id: Int! 71 | commander: Character! 72 | name: String! 73 | size: Float! 74 | crew: [Character!]! 75 | designation: String! 76 | } 77 | type StarshipSimple2 { 78 | id: Int! 79 | } 80 | type StarshipSimple3 { 81 | series: [Series]! 82 | } 83 | interface Character { 84 | isDead: Boolean! 85 | ships: Starship 86 | series: [Series!]! 87 | id: Int! 88 | ship: Starship 89 | name: String! 90 | allwaysNull: Starship 91 | commands: [Character!]! 92 | alsoAllwaysNull: Int 93 | someOldField: Int! @deprecated(reason: "Stupid name") 94 | commanders: [Character!]! 95 | } 96 | input AddCrewmanData { 97 | shipId: Int! 98 | location: Vector! 99 | series: [Series!]! 100 | name: String! 101 | } 102 | input Vector { 103 | y: Float! 104 | x: Float! 105 | } 106 | type Humanoid implements Character { 107 | isDead: Boolean! 108 | ships: Starship 109 | series: [Series!]! 110 | id: Int! 111 | ship: Starship 112 | dateOfBirth: Date! 113 | name: String! 114 | species: String! 115 | allwaysNull: Starship 116 | commands: [Character!]! 117 | alsoAllwaysNull: Int 118 | someOldField: Int! @deprecated(reason: "Stupid name") 119 | commanders: [Character!]! 120 | } 121 | scalar Date 122 | union SearchResult = Humanoid | Android | Starship 123 | scalar DateTime 124 | -------------------------------------------------------------------------------- /test/schema2.gql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: queryType 3 | } 4 | type queryType { 5 | maybeNull: Int 6 | small: Small 7 | manysmall: [Small!]! 8 | name: String! 9 | neverNull: Int! 10 | foo: Int! 11 | bar: Int! 12 | } 13 | type Small { 14 | name: String! 15 | arg(a: Int!): [SmallChild!]! 16 | id: Int! 17 | foobar: [SmallChild!]! 18 | } 19 | type SmallChild { 20 | foo(a: Int!): Int! 21 | id: Int! 22 | name: String! 23 | } 24 | -------------------------------------------------------------------------------- /test/source/testdata.d: -------------------------------------------------------------------------------- 1 | module testdata; 2 | 3 | import std.stdio; 4 | import std.datetime : DateTime, Date; 5 | import std.conv : to; 6 | import std.array : array, back; 7 | import std.format : format; 8 | import std.algorithm : each, map, joiner; 9 | import std.range : tee; 10 | import std.typecons : nullable, Nullable; 11 | 12 | import vibe.data.json; 13 | 14 | import nullablestore; 15 | 16 | import graphql.testschema; 17 | import graphql.helper : returnTemplate; 18 | import graphql.uda; 19 | 20 | @safe: 21 | 22 | // The database impl 23 | 24 | Json characterToJson(Character c) { 25 | Json ret = Json.emptyObject(); 26 | ret["data"] = Json.emptyObject(); 27 | 28 | // direct 29 | ret["data"]["id"] = c.id; 30 | ret["data"]["name"] = c.name; 31 | ret["data"]["series"] = Json.emptyArray(); 32 | ret["data"]["__typename"] = "Character"; 33 | foreach(Series s; c.series) { 34 | ret["data"]["series"] ~= to!string(s); 35 | } 36 | 37 | // indirect 38 | ret["data"]["commandsIds"] = Json.emptyArray(); 39 | foreach(Character cc; c.commands) { 40 | ret["data"]["commandsIds"] ~= cc.id; 41 | } 42 | 43 | // indirect 44 | if(c.ship.isNull()) { 45 | ret["data"]["shipId"] = Json(null); 46 | } else { 47 | ret["data"]["shipId"] = c.ship.get().id; 48 | } 49 | 50 | // indirect 51 | ret["data"]["commandersIds"] = Json.emptyArray(); 52 | foreach(Character cc; c.commanders) { 53 | ret["data"]["commandersIds"] ~= cc.id; 54 | } 55 | 56 | if(Humanoid h = cast(Humanoid)c) { 57 | ret["data"]["species"] = h.species; 58 | //ret["data"]["__typename"] = "Humanoid"; TODO reverse hack 59 | ret["data"]["__typename"] = "Character"; 60 | ret["data"]["dateOfBirth"] = Json(h.dateOfBirth.toISOExtString()); 61 | } 62 | 63 | if(Android a = cast(Android)c) { 64 | ret["data"]["primaryFunction"] = a.primaryFunction; 65 | ret["data"]["__typename"] = "Android"; 66 | } 67 | 68 | return ret; 69 | } 70 | 71 | 72 | class CharacterImpl : Character { 73 | @GQLDUda(Ignore.yes) 74 | this(long id, string name) { 75 | this.id = id; 76 | this.name = name; 77 | } 78 | } 79 | 80 | class HumanoidImpl : Humanoid { 81 | @GQLDUda(Ignore.yes) 82 | this(long id, string name, string species, Date dob) { 83 | this.id = id; 84 | this.name = name; 85 | this.species = species; 86 | this.dateOfBirth = dob; 87 | } 88 | 89 | @GQLDUda(Ignore.yes) 90 | override string toString() const @safe { 91 | return format!("Humanoid(id(%d), name(%s), species(%s), " 92 | ~ " series[%(%s,%)], commands[%(%s,%)], ship(%s))," 93 | ~ " commanders[%(%s,%)]") 94 | ( 95 | id, name, species, series, commands.map!(a => a.name), 96 | !ship.isNull() ? ship.get().designation : "" 97 | , commanders.map!(a => a.name) 98 | ); 99 | } 100 | } 101 | 102 | class AndroidImpl : Android { 103 | this(long id, string name, string pfunc) { 104 | this.id = id; 105 | this.name = name; 106 | this.primaryFunction = pfunc; 107 | } 108 | 109 | @GQLDUda(Ignore.yes) 110 | override string toString() const @safe { 111 | return format!("Android(id(%d), name(%s), function(%s), series[%(%s,%)], " 112 | ~ " commands[%(%s,%)], ship(%s)), commanders[%(%s,%)]") 113 | ( 114 | id, name, primaryFunction, series, commands.map!(a => a.name), 115 | !ship.isNull() ? ship.get().designation : "", 116 | commanders.map!(a => a.name) 117 | ); 118 | } 119 | } 120 | 121 | Json starshipToJson(Starship s) { 122 | Json ret = returnTemplate(); 123 | 124 | // direct 125 | ret["data"]["id"] = s.id; 126 | ret["data"]["designation"] = s.designation; 127 | ret["data"]["name"] = s.name; 128 | ret["data"]["size"] = s.size; 129 | ret["data"]["series"] = Json.emptyArray(); 130 | ret["data"]["__typename"] = "Starship"; 131 | foreach(Nullable!Series show; s.series) { 132 | if(!show.isNull()) { 133 | ret["data"]["series"] ~= to!string(show.get()); 134 | } 135 | } 136 | 137 | // indirect 138 | ret["data"]["commanderId"] = s.commander.id; 139 | ret["data"]["crewIds"] = Json.emptyArray(); 140 | foreach(Character cm; s.crew) { 141 | ret["data"]["crewIds"] ~= cm.id; 142 | } 143 | 144 | //logf("%s", ret.toPrettyString()); 145 | return ret; 146 | } 147 | 148 | class Data { 149 | Character[] chars; 150 | Starship[] ships; 151 | long i; 152 | 153 | @GQLDUda(Ignore.yes) 154 | this() @trusted { 155 | auto picard = new HumanoidImpl(i++, "Jean-Luc Picard", "Human", 156 | Date(2305, 7, 13)); 157 | auto worf = new HumanoidImpl(i++, "Worf", "Klingon", Date(2340, 5, 23)); 158 | picard.series ~= Series.TheNextGeneration; 159 | auto tng = [ 160 | new HumanoidImpl(i++, "William Riker", "Human", Date(2335, 8, 19)), 161 | new HumanoidImpl(i++, "Deanna Troi", "Betazoid", Date(2336, 3, 29)), 162 | new HumanoidImpl(i++, "Dr. Beverly Crusher", "Human", 163 | Date(2324, 10, 13)), 164 | worf, 165 | new AndroidImpl(i++, "Data", "Becoming Human"), 166 | new HumanoidImpl(i++, "Geordi La Forge", "Human", Date(2335, 2, 16)), 167 | new HumanoidImpl(i++, "Miles O'Brien", "Human", Date(2328, 9, 1)) 168 | ]; 169 | picard.commands = tng; 170 | picard.series ~= Series.DeepSpaceNine; 171 | tng.each!(a => a.series ~= Series.TheNextGeneration); 172 | tng.each!(a => a.commanders ~= picard); 173 | tng[0].series ~= Series.Enterprise; 174 | tng[1].series ~= Series.Voyager; 175 | tng[1].series ~= Series.Enterprise; 176 | tng[3].series ~= Series.DeepSpaceNine; 177 | tng[6].series ~= Series.DeepSpaceNine; 178 | 179 | auto sisko = new HumanoidImpl(i++, "Benjamin Sisko", "Human", 180 | Date(2332, 1, 1)); 181 | sisko.series ~= Series.DeepSpaceNine; 182 | auto ds9 = [ 183 | new HumanoidImpl(i++, "Odo", "Changeling", Date(1970, 1, 1)), 184 | new HumanoidImpl(i++, "Jadzia Dax", "Trill", Date(2346, 8, 20)), 185 | new HumanoidImpl(i++, "Dr. Julian Bashir", "Human", Date(2341, 1, 1)), 186 | worf, 187 | new HumanoidImpl(i++, "Kira Nerys", "Bajoran", Date(2343, 1, 1)), 188 | new HumanoidImpl(i++, "Elim Garak", "Cardassian", Date(2320, 1, 1)) 189 | ]; 190 | sisko.commands = cast(Character[])ds9; 191 | ds9.each!(a => a.series ~= Series.DeepSpaceNine); 192 | ds9.each!(a => a.commanders ~= sisko); 193 | 194 | tng[6].commanders ~= sisko; 195 | 196 | auto janeway = new HumanoidImpl(i++, "Kathryn Janeway", "Human", 197 | Date(2330, 5, 20)); 198 | auto voyager = [ 199 | new HumanoidImpl(i++, "Chakotay", "Human", Date(2329, 1, 1)), 200 | new HumanoidImpl(i++, "Tuvok", "Vulcan", Date(2361, 10, 10)), 201 | new HumanoidImpl(i++, "Neelix", "Talaxian", Date(2340, 1, 1)), 202 | new HumanoidImpl(i++, "Seven of Nine", "Human", Date(2348, 6, 24)), 203 | new HumanoidImpl(i++, "B'Elanna Torres", "Klingon", Date(2349, 2, 1)), 204 | new HumanoidImpl(i++, "Tom Paris", "Human", Date(2346, 2, 1)), 205 | new HumanoidImpl(i++, "Harry Kim", "Human", Date(2349, 2, 1)), 206 | ]; 207 | janeway.commands = cast(Character[])voyager; 208 | voyager.each!(a => a.series ~= Series.Voyager); 209 | voyager.each!(a => a.commanders ~= janeway); 210 | 211 | auto archer = new HumanoidImpl(i++, "Jonathan Archer", "Human", 212 | Date(2112, 10, 9)); 213 | auto enterprise = [ 214 | new HumanoidImpl(i++, "Charles Tucker III", "Human", Date(2121, 2, 215 | 1)), 216 | new HumanoidImpl(i++, "Hoshi Sato", "Human", Date(2129, 7, 9)), 217 | new HumanoidImpl(i++, "Dr. Phlox", "Denobulan", Date(2080, 2, 1)), 218 | new HumanoidImpl(i++, "Malcolm Reed", "Human", Date(2117, 9, 2)), 219 | new HumanoidImpl(i++, "Travis Mayweather", "Human", Date(2121, 2, 1)), 220 | new HumanoidImpl(i++, "T'Pol", "Vulcan", Date(2088, 2, 1)) 221 | ]; 222 | archer.commands = cast(Character[])enterprise; 223 | enterprise.each!(a => a.series ~= Series.Enterprise); 224 | 225 | auto kirk = new HumanoidImpl(i++, "James T. Kirk", "Human", 226 | Date(2202, 2, 1)); 227 | auto tos = [ 228 | new HumanoidImpl(i++, "Hikaru Sulu", "Human", Date(2237, 2, 1)), 229 | new HumanoidImpl(i++, "Uhura", "Human", Date(2239, 2, 1)), 230 | new HumanoidImpl(i++, "Montgomery Scott", "Human", Date(2222, 2, 1)), 231 | new HumanoidImpl(i++, "Dr. Leonard McCoy", "Human", Date(2227, 2, 1)), 232 | new HumanoidImpl(i++, "Spock", "Vulcan", Date(2230, 2, 1)), 233 | ]; 234 | kirk.commands = cast(Character[])tos; 235 | tos.each!(a => a.series ~= Series.TheOriginalSeries); 236 | tos.each!(a => a.commanders ~= kirk); 237 | 238 | auto georgiou = new HumanoidImpl(i++, "Philippa Georgiou", "Human", 239 | Date(2202, 2, 1)); 240 | auto discovery = [ 241 | new HumanoidImpl(i++, "Michael Burnham", "Human", Date(2226, 2, 1)), 242 | new HumanoidImpl(i++, "Paul Stamets", "Human", Date(2225, 2, 1)), 243 | new HumanoidImpl(i++, "Sylvia Tilly", "Human", Date(2230, 2, 1)), 244 | new HumanoidImpl(i++, "Ash Tyler", "Klingon", Date(2230, 2, 1)), 245 | new HumanoidImpl(i++, "Saru", "Kelpien", Date(2230, 2, 1)), 246 | new HumanoidImpl(i++, "Hugh Culber", "Human", Date(2227, 2, 1)), 247 | ]; 248 | georgiou.commands = cast(Character[])discovery; 249 | discovery.each!(a => a.series ~= Series.Discovery); 250 | discovery.each!(a => a.commanders ~= georgiou); 251 | 252 | this.ships ~= new Starship(i++, "NCC-1701E", 685.7, "Enterprise"); 253 | this.ships.back.series ~= nullable(Series.TheNextGeneration); 254 | this.ships.back.commander = picard; 255 | this.ships.back.crew = tng; 256 | this.ships.back.crew ~= picard; 257 | tng.each!(a => a.ship = nullable(this.ships.back)); 258 | 259 | this.ships ~= new Starship(i++, "NX-74205", 130.0, "Defiant"); 260 | this.ships.back.series ~= nullable(Series.DeepSpaceNine); 261 | this.ships.back.series ~= nullable(Series.TheOriginalSeries); 262 | this.ships.back.commander = sisko; 263 | this.ships.back.crew = cast(Character[])ds9; 264 | this.ships.back.crew ~= sisko; 265 | ds9.each!(a => a.ship = nullable(this.ships.back)); 266 | 267 | this.ships ~= new Starship(i++, "NCC-74656", 343.0, "Voyager"); 268 | this.ships.back.series ~= nullable(Series.Voyager); 269 | this.ships.back.series ~= nullable(Series.DeepSpaceNine); 270 | this.ships.back.commander = janeway; 271 | this.ships.back.crew = cast(Character[])voyager; 272 | this.ships.back.crew ~= janeway; 273 | voyager.each!(a => a.ship = nullable(this.ships.back)); 274 | 275 | this.ships ~= new Starship(i++, "NX-01", 225.0, "Enterprise"); 276 | this.ships.back.series ~= nullable(Series.Enterprise); 277 | this.ships.back.commander = archer; 278 | this.ships.back.crew = cast(Character[])enterprise; 279 | this.ships.back.crew ~= archer; 280 | enterprise.each!(a => a.ship = nullable(this.ships.back)); 281 | 282 | this.ships ~= new Starship(i++, "NCC-1701", 288.64, "Enterprise"); 283 | this.ships.back.series ~= nullable(Series.TheOriginalSeries); 284 | this.ships.back.series ~= nullable(Series.Discovery); 285 | this.ships.back.commander = kirk; 286 | this.ships.back.crew = cast(Character[])tos; 287 | this.ships.back.crew ~= kirk; 288 | tos.each!(a => a.ship = nullable(this.ships.back)); 289 | 290 | this.ships ~= new Starship(i++, "NCC-1031", 244.00, "Discovery"); 291 | this.ships.back.series ~= nullable(Series.Discovery); 292 | this.ships.back.commander = georgiou; 293 | this.ships.back.crew = cast(Character[])discovery; 294 | this.ships.back.crew ~= georgiou; 295 | discovery.each!(a => a.ship = nullable(this.ships.back)); 296 | 297 | this.chars = 298 | joiner([ 299 | cast(Character[])[picard, sisko, janeway, archer, kirk, georgiou], 300 | cast(Character[])tng, cast(Character[])ds9, 301 | cast(Character[])voyager, cast(Character[])enterprise, 302 | cast(Character[])tos, cast(Character[])discovery 303 | ]) 304 | .array; 305 | } 306 | } 307 | 308 | unittest { 309 | auto d = new Data(); 310 | } 311 | -------------------------------------------------------------------------------- /test/source/testdata2.d: -------------------------------------------------------------------------------- 1 | module testdata2; 2 | 3 | import std.typecons : Nullable; 4 | 5 | import graphql.schema.directives; 6 | 7 | abstract class SmallChild { 8 | long id; 9 | string name; 10 | int foo(long a); 11 | } 12 | 13 | abstract class Small { 14 | long id; 15 | string name; 16 | abstract SmallChild[] arg(int a); 17 | SmallChild[] foobar; 18 | } 19 | 20 | interface Query2 { 21 | long foo(); 22 | long bar(); 23 | Nullable!Small small(); 24 | Small[] manysmall(); 25 | 26 | Nullable!long maybeNull(); 27 | long neverNull(); 28 | } 29 | 30 | class Schema2 { 31 | Query2 queryType; 32 | DefaultDirectives directives; 33 | //Mutation2 mutation; 34 | //Subscription2 subscription; 35 | } 36 | -------------------------------------------------------------------------------- /test/source/testqueries.d: -------------------------------------------------------------------------------- 1 | module testqueries; 2 | 3 | import std.typecons : Flag; 4 | 5 | alias ShouldThrow = Flag!"ShouldThrow"; 6 | 7 | struct TestQuery { 8 | string query; 9 | ShouldThrow st; 10 | string expectedResult; 11 | string variables; 12 | } 13 | 14 | TestQuery[] queries = [ 15 | TestQuery(` 16 | { 17 | starships(overSize: 600) { 18 | commander { 19 | allwaysNull { 20 | id 21 | } 22 | } 23 | } 24 | }`, ShouldThrow.no, 25 | `{ 26 | "starships" : [ 27 | { 28 | "commander" : { 29 | "allwaysNull": null 30 | } 31 | } 32 | ] 33 | } 34 | `), 35 | // identical to the previous query, but contains a carriage return to ensure 36 | // that they're parsed correctly 37 | TestQuery(" 38 | { 39 | starships(overSize: 600) {\r 40 | commander { 41 | allwaysNull { 42 | id 43 | } 44 | } 45 | } 46 | }", ShouldThrow.no, 47 | `{ 48 | "starships" : [ 49 | { 50 | "commander" : { 51 | "allwaysNull": null 52 | } 53 | } 54 | ] 55 | } 56 | `), 57 | TestQuery(` 58 | { 59 | alwaysEmpty { id } 60 | } 61 | `, ShouldThrow.no, 62 | `{ 63 | "alwaysEmpty": [] 64 | } 65 | `), 66 | TestQuery(` 67 | { 68 | starships(overSize: 600) { 69 | commander { 70 | alsoAllwaysNull 71 | } 72 | } 73 | }`, ShouldThrow.no, 74 | `{ 75 | "starships" : [ 76 | { 77 | "commander" : { 78 | "alsoAllwaysNull": null 79 | } 80 | } 81 | ] 82 | } 83 | `), 84 | TestQuery(` 85 | query IntrospectionQuery { 86 | __schema { 87 | queryType { name } 88 | mutationType { name } 89 | subscriptionType { name } 90 | types { 91 | ...FullType 92 | } 93 | directives { 94 | name 95 | description 96 | locations 97 | args { 98 | ...InputValue 99 | } 100 | } 101 | } 102 | } 103 | 104 | fragment FullType on __Type { 105 | kind 106 | name 107 | description 108 | fields(includeDeprecated: true) { 109 | name 110 | description 111 | args { 112 | ...InputValue 113 | } 114 | type { 115 | ...TypeRef 116 | } 117 | isDeprecated 118 | deprecationReason 119 | } 120 | inputFields { 121 | ...InputValue 122 | } 123 | interfaces { 124 | ...TypeRef 125 | } 126 | enumValues(includeDeprecated: true) { 127 | name 128 | description 129 | isDeprecated 130 | deprecationReason 131 | } 132 | possibleTypes { 133 | ...TypeRef 134 | } 135 | } 136 | 137 | fragment InputValue on __InputValue { 138 | name 139 | description 140 | type { ...TypeRef } 141 | defaultValue 142 | } 143 | 144 | fragment TypeRef on __Type { 145 | kind 146 | name 147 | ofType { 148 | kind 149 | name 150 | ofType { 151 | kind 152 | name 153 | ofType { 154 | kind 155 | name 156 | ofType { 157 | kind 158 | name 159 | ofType { 160 | kind 161 | name 162 | ofType { 163 | kind 164 | name 165 | ofType { 166 | kind 167 | name 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | `, ShouldThrow.no) 177 | , 178 | TestQuery(` 179 | query a { 180 | starships(overSize: 10) { 181 | name 182 | crew { 183 | ...hyooman 184 | ...robot 185 | ...charac 186 | } 187 | } 188 | } 189 | 190 | fragment hyooman on Humanoid { 191 | species 192 | dateOfBirth 193 | } 194 | 195 | fragment robot on Android { 196 | primaryFunction 197 | } 198 | 199 | fragment charac on Character { 200 | ...robot 201 | id 202 | ...hyooman 203 | name 204 | series 205 | } 206 | `, ShouldThrow.no), 207 | TestQuery(` 208 | { 209 | starships 210 | } 211 | `, ShouldThrow.yes), 212 | TestQuery(` 213 | { 214 | starships { 215 | designation 216 | name 217 | size 218 | } 219 | 220 | starship(id: 44) { 221 | designation 222 | } 223 | currentTime 224 | } 225 | 226 | `, ShouldThrow.no), 227 | TestQuery(` 228 | query a { 229 | __type(name: "Starship") { 230 | ...FullType 231 | } 232 | } 233 | 234 | fragment FullType on __Type { 235 | kind 236 | name 237 | description 238 | fields(includeDeprecated: true) { 239 | name 240 | description 241 | args { 242 | ...InputValue 243 | } 244 | type { 245 | ...TypeRef 246 | } 247 | isDeprecated 248 | deprecationReason 249 | } 250 | inputFields { 251 | ...InputValue 252 | } 253 | interfaces { 254 | ...TypeRef 255 | } 256 | enumValues(includeDeprecated: true) { 257 | name 258 | description 259 | isDeprecated 260 | deprecationReason 261 | } 262 | possibleTypes { 263 | ...TypeRef 264 | } 265 | } 266 | 267 | fragment InputValue on __InputValue { 268 | name 269 | description 270 | type { 271 | ...TypeRef 272 | } 273 | defaultValue 274 | } 275 | 276 | fragment TypeRef on __Type { 277 | kind 278 | name 279 | ofType { 280 | kind 281 | name 282 | ofType { 283 | kind 284 | name 285 | ofType { 286 | kind 287 | name 288 | ofType { 289 | kind 290 | name 291 | ofType { 292 | kind 293 | name 294 | ofType { 295 | kind 296 | name 297 | ofType { 298 | kind 299 | name 300 | } 301 | } 302 | } 303 | } 304 | } 305 | } 306 | } 307 | } 308 | `, ShouldThrow.no), 309 | TestQuery(` 310 | { 311 | shipsselection(ids:[42,45]) { 312 | id 313 | commander { 314 | name 315 | } 316 | __typename 317 | designation 318 | } 319 | } 320 | `, ShouldThrow.no), 321 | TestQuery(` 322 | mutation one { 323 | #addCrewman(input: {name: "Robert", shipId: 44, series: [Enterprise, DeepSpaceNice]}) { 324 | addCrewman(input: {name: "Robert", shipId: 44, series: [Enterprise, DeepSpaceNice]}) { 325 | id 326 | name 327 | ship { 328 | id 329 | designation 330 | } 331 | } 332 | } 333 | `, ShouldThrow.no), 334 | TestQuery(` 335 | { 336 | starshipDoesNotExist { 337 | id 338 | commander { 339 | name 340 | } 341 | } 342 | }`, ShouldThrow.yes, 343 | `{ 344 | "errors" : [ 345 | { "message" : "That ship does not exists" 346 | , "path" : ["SelectionSet", "starshipDoesNotExist"] 347 | } 348 | ], 349 | "data" : { 350 | "starshipDoesNotExist" : null 351 | } 352 | }` 353 | ), 354 | TestQuery(` 355 | { 356 | resolverWillThrow { 357 | primaryFunction 358 | } 359 | }`, ShouldThrow.yes, 360 | `{ 361 | "errors" : [ 362 | { "message": "you can not pass" 363 | , "path" : ["SelectionSet", "resolverWillThrow"] 364 | } 365 | ], 366 | "data" : { 367 | "resolverWillThrow": null 368 | } 369 | }` 370 | ), 371 | TestQuery(` 372 | { 373 | search(name: "Enterprise") { 374 | ... on Starship { 375 | designation 376 | size 377 | } 378 | } 379 | }`, ShouldThrow.no, 380 | `{ 381 | "search": { 382 | "size": 685.7, 383 | "designation": "NCC-1701E" 384 | } 385 | }` 386 | ), 387 | TestQuery(` 388 | { 389 | search(name: "Enterprise") { 390 | ... on Starship { 391 | name 392 | } 393 | ... on Character { 394 | name 395 | } 396 | } 397 | }`, ShouldThrow.no 398 | ), 399 | TestQuery(` 400 | { 401 | search(name: "Enterprise") { 402 | ... on Starship { 403 | name 404 | } 405 | ... on Character { 406 | doesNotExist_FooBar 407 | } 408 | } 409 | }`, ShouldThrow.yes 410 | ), 411 | TestQuery(` 412 | query s($v: Boolean!) { 413 | search(name: "Enterprise") @if(if: $v){ 414 | ... on Starship { 415 | name 416 | } 417 | } 418 | }`, ShouldThrow.yes 419 | , "Variable with name 'v' required available " 420 | , `{"b" : false }` 421 | ) 422 | ]; 423 | --------------------------------------------------------------------------------