├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── graphql.rs ├── bulk.yaml ├── src ├── common.rs ├── format.rs ├── helpers.rs ├── lib.rs ├── position.rs ├── query │ ├── ast.rs │ ├── error.rs │ ├── format.rs │ ├── grammar.rs │ ├── minify.rs │ └── mod.rs ├── schema │ ├── ast.rs │ ├── error.rs │ ├── format.rs │ ├── grammar.rs │ └── mod.rs └── tokenizer.rs ├── tests ├── queries │ ├── directive_args.graphql │ ├── directive_args_multiline.graphql │ ├── fragment.graphql │ ├── fragment_spread.graphql │ ├── inline_fragment.graphql │ ├── inline_fragment_dir.graphql │ ├── kitchen-sink.graphql │ ├── kitchen-sink_canonical.graphql │ ├── minimal.graphql │ ├── minimal_mutation.graphql │ ├── minimal_query.graphql │ ├── mutation_directive.graphql │ ├── mutation_nameless_vars.graphql │ ├── named_query.graphql │ ├── nested_selection.graphql │ ├── query_aliases.graphql │ ├── query_arguments.graphql │ ├── query_arguments_multiline.graphql │ ├── query_array_argument_multiline.graphql │ ├── query_directive.graphql │ ├── query_list_argument.graphql │ ├── query_nameless_vars.graphql │ ├── query_nameless_vars_multiple_fields.graphql │ ├── query_nameless_vars_multiple_fields_canonical.graphql │ ├── query_object_argument.graphql │ ├── query_object_argument_multiline.graphql │ ├── query_var_default_float.graphql │ ├── query_var_default_list.graphql │ ├── query_var_default_object.graphql │ ├── query_var_default_string.graphql │ ├── query_var_defaults.graphql │ ├── query_vars.graphql │ ├── string_literal.graphql │ ├── subscription_directive.graphql │ └── triple_quoted_literal.graphql ├── query_errors.rs ├── query_errors │ ├── bad_args.txt │ └── invalid_curly_brace.txt ├── query_roundtrips.rs ├── schema_roundtrips.rs └── schemas │ ├── directive.graphql │ ├── directive_descriptions.graphql │ ├── directive_descriptions_canonical.graphql │ ├── directive_variable_definition.graphql │ ├── empty_union.graphql │ ├── enum.graphql │ ├── extend_enum.graphql │ ├── extend_input.graphql │ ├── extend_input_canonical.graphql │ ├── extend_interface.graphql │ ├── extend_object.graphql │ ├── extend_scalar.graphql │ ├── implements.graphql │ ├── implements_amp.graphql │ ├── implements_amp_canonical.graphql │ ├── implements_interface.graphql │ ├── input_type.graphql │ ├── interface.graphql │ ├── kitchen-sink.graphql │ ├── kitchen-sink_canonical.graphql │ ├── minimal.graphql │ ├── minimal_type.graphql │ ├── repeatable.graphql │ ├── scalar_type.graphql │ ├── simple_object.graphql │ ├── union.graphql │ └── union_extension.graphql └── vagga.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.rs] 2 | end_of_line = lf 3 | charset = utf-8 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out repository code 15 | uses: actions/checkout@v2 16 | 17 | - name: Cache Rust 18 | uses: Swatinem/rust-cache@v1 19 | 20 | - name: Test Rust 21 | run: cargo test 22 | 23 | rustfmt: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: Cache Rust 29 | uses: Swatinem/rust-cache@v1 30 | 31 | - run: cargo fmt --all --check 32 | 33 | toml_validation: 34 | runs-on: ubuntu-latest 35 | container: 36 | image: tamasfe/taplo:0.8.1 37 | steps: 38 | - name: Checkout sources 39 | uses: actions/checkout@v4 40 | - name: taplo lint 41 | run: taplo lint 42 | - name: taplo fmt 43 | run: taplo fmt --check --diff 44 | 45 | msrv: 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: taiki-e/install-action@cargo-hack 50 | - uses: Swatinem/rust-cache@v2 51 | - name: check with all-features 52 | run: cargo hack --rust-version --no-private --no-dev-deps check --all-features 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | /.vagga 3 | /target 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: rust 4 | 5 | cache: 6 | - cargo 7 | 8 | before_cache: 9 | - rm -r $TRAVIS_BUILD_DIR/target/debug 10 | 11 | jobs: 12 | include: 13 | - os: linux 14 | rust: stable 15 | - os: linux 16 | rust: beta 17 | - os: linux 18 | rust: nightly 19 | 20 | # deploy 21 | - stage: publish 22 | os: linux 23 | rust: stable 24 | env: 25 | # CARGO_TOKEN 26 | - secure: "qHOydmz3Un/32JFlWEGYc4VzqKNBrOODOrVQxKO6u6NqTDo+4vGK7xLtct5rx58vF77BvSdfiR6RRcqIuYlWKNA/WbPCTolmy2cAkSztzjhpvmyF18LoM32gdaaRJ5+6Zay64/vG5LtE5QhIhFzmbJ6opBVv9PaQR1ptI/0Ss+gyvI7jw5WZAoUV4YD5ODTacdCAcoVohHHQcSxpMMVWQp7hFf8ZWwVVYQOZF44AZ3N9BHRc9MKr9XQ70E+TvHJjGRdwJIX6mNpWlI43JwkN2DeVA6cA/eWh91zLWkmkzNuYW63He02ueS9iTXvkoZnQysSMgB7TNNkP4TtlY2udvL7tO2UXt+6Wbh1kMESrtsvUSK9363Q/lrVSUKV3DZUFQC8CMh3Enn+W/2hyE/YDghcip+P7FHbrTf4TuWUzSWXA20xBMn/kkTWzX4gchTcrRKqW7RZHNZpOOplGvnW5JfGnRKC9CDx+kT+IGrKvnVsnRNqGI8bAm5SKgu6Nqr9TLxIQ6IKv/FAuQSjKh7l6STwNVPv5q3bji8zmEeSonKhSCbtSfHRepnY16mirM3Fw5f1beoDZR0qwsaIHnJz0RS0Mw7iuB0mt0bnpqgrl/agLmrjPrRQtPo01eqnli8ZU5cSpAND7B+ZWfqRFGdekyXsKJNAWycqAjOjg0qCjJEA=" 27 | install: true 28 | script: true 29 | 30 | deploy: 31 | - provider: script 32 | script: 'cargo publish --verbose --token=$CARGO_TOKEN' 33 | on: 34 | tags: true 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql-parser" 3 | description = """ 4 | A parser, AST and serializer for graphql query language and scheme 5 | definition language (sometimes called IDL). 6 | """ 7 | license = "MIT/Apache-2.0" 8 | readme = "README.md" 9 | keywords = ["graphql", "parser"] 10 | categories = ["parser-implementations", "command-line-interface"] 11 | homepage = "https://github.com/graphql-rust/graphql-parser" 12 | documentation = "https://docs.rs/graphql-parser" 13 | version = "0.4.1" 14 | authors = ["Paul Colomiets "] 15 | edition = "2018" 16 | rust-version = "1.61" 17 | 18 | [dependencies] 19 | combine = "4.6.6" 20 | thiserror = "2" 21 | 22 | [dev-dependencies] 23 | pretty_assertions = "0.5.0" 24 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The graphql-parser Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GraphQL Parser 2 | ============== 3 | 4 | [Documentation](https://docs.rs/graphql-parser) | 5 | [Github](https://github.com/tailhook/graphql-parser) | 6 | [Crate](https://crates.io/crates/graphql-parser) 7 | 8 | A parser, formatter and AST for graphql query and schema definition language 9 | for rust. 10 | 11 | Supported extensions: 12 | 13 | 1. Subscriptions 14 | 2. Block (triple quoted) strings 15 | 16 | 17 | License 18 | ======= 19 | 20 | Licensed under either of 21 | 22 | * Apache License, Version 2.0, 23 | (./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 24 | * MIT license (./LICENSE-MIT or http://opensource.org/licenses/MIT) 25 | at your option. 26 | 27 | Contribution 28 | ------------ 29 | 30 | Unless you explicitly state otherwise, any contribution intentionally 31 | submitted for inclusion in the work by you, as defined in the Apache-2.0 32 | license, shall be dual licensed as above, without any additional terms or 33 | conditions. 34 | 35 | -------------------------------------------------------------------------------- /benches/graphql.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | extern crate graphql_parser; 5 | 6 | use std::fs::File; 7 | use std::io::Read; 8 | 9 | use graphql_parser::parse_query; 10 | 11 | fn load_file(name: &str) -> String { 12 | let mut buf = String::with_capacity(1024); 13 | let path = format!("tests/queries/{}.graphql", name); 14 | let mut f = File::open(path).unwrap(); 15 | f.read_to_string(&mut buf).unwrap(); 16 | buf 17 | } 18 | 19 | #[bench] 20 | fn bench_minimal(b: &mut test::Bencher) { 21 | let f = load_file("minimal"); 22 | b.iter(|| parse_query::(&f).unwrap()); 23 | } 24 | 25 | #[bench] 26 | fn bench_inline_fragment(b: &mut test::Bencher) { 27 | let f = load_file("inline_fragment"); 28 | b.iter(|| parse_query::(&f).unwrap()); 29 | } 30 | 31 | #[bench] 32 | fn bench_directive_args(b: &mut test::Bencher) { 33 | let f = load_file("directive_args"); 34 | b.iter(|| parse_query::(&f).unwrap()); 35 | } 36 | 37 | #[bench] 38 | fn bench_query_vars(b: &mut test::Bencher) { 39 | let f = load_file("query_vars"); 40 | b.iter(|| parse_query::(&f).unwrap()); 41 | } 42 | 43 | #[bench] 44 | fn bench_kitchen_sink(b: &mut test::Bencher) { 45 | let f = load_file("kitchen-sink"); 46 | b.iter(|| parse_query::(&f).unwrap()); 47 | } 48 | -------------------------------------------------------------------------------- /bulk.yaml: -------------------------------------------------------------------------------- 1 | minimum-bulk: v0.4.5 2 | 3 | versions: 4 | 5 | - file: Cargo.toml 6 | block-start: ^\[package\] 7 | block-end: ^\[.*\] 8 | regex: ^version\s*=\s*"(\S+)" 9 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, fmt}; 2 | 3 | use combine::easy::{Error, Info}; 4 | use combine::{choice, many, many1, optional, position, StdParseResult}; 5 | use combine::{parser, Parser}; 6 | 7 | use crate::helpers::{ident, kind, name, punct}; 8 | use crate::position::Pos; 9 | use crate::tokenizer::{Kind as T, Token, TokenStream}; 10 | 11 | /// Text abstracts over types that hold a string value. 12 | /// It is used to make the AST generic over the string type. 13 | pub trait Text<'a>: 'a { 14 | type Value: 'a 15 | + From<&'a str> 16 | + AsRef 17 | + std::borrow::Borrow 18 | + PartialEq 19 | + Eq 20 | + PartialOrd 21 | + Ord 22 | + fmt::Debug 23 | + Clone; 24 | } 25 | 26 | impl<'a> Text<'a> for &'a str { 27 | type Value = Self; 28 | } 29 | 30 | impl<'a> Text<'a> for String { 31 | type Value = String; 32 | } 33 | 34 | impl<'a> Text<'a> for std::borrow::Cow<'a, str> { 35 | type Value = Self; 36 | } 37 | 38 | #[derive(Debug, Clone, PartialEq)] 39 | pub struct Directive<'a, T: Text<'a>> { 40 | pub position: Pos, 41 | pub name: T::Value, 42 | pub arguments: Vec<(T::Value, Value<'a, T>)>, 43 | } 44 | 45 | /// This represents integer number 46 | /// 47 | /// But since there is no definition on limit of number in spec 48 | /// (only in implemetation), we do a trick similar to the one 49 | /// in `serde_json`: encapsulate value in new-type, allowing type 50 | /// to be extended later. 51 | #[derive(Debug, Clone, PartialEq)] 52 | // we use i64 as a reference implementation: graphql-js thinks even 32bit 53 | // integers is enough. We might consider lift this limit later though 54 | pub struct Number(pub(crate) i64); 55 | 56 | #[derive(Debug, Clone, PartialEq)] 57 | pub enum Value<'a, T: Text<'a>> { 58 | Variable(T::Value), 59 | Int(Number), 60 | Float(f64), 61 | String(String), 62 | Boolean(bool), 63 | Null, 64 | Enum(T::Value), 65 | List(Vec>), 66 | Object(BTreeMap>), 67 | } 68 | 69 | impl<'a, T: Text<'a>> Value<'a, T> { 70 | pub fn into_static(&self) -> Value<'static, String> { 71 | match self { 72 | Self::Variable(v) => Value::Variable(v.as_ref().into()), 73 | Self::Int(i) => Value::Int(i.clone()), 74 | Self::Float(f) => Value::Float(*f), 75 | Self::String(s) => Value::String(s.clone()), 76 | Self::Boolean(b) => Value::Boolean(*b), 77 | Self::Null => Value::Null, 78 | Self::Enum(v) => Value::Enum(v.as_ref().into()), 79 | Self::List(l) => Value::List(l.iter().map(|e| e.into_static()).collect()), 80 | Self::Object(o) => Value::Object( 81 | o.iter() 82 | .map(|(k, v)| (k.as_ref().into(), v.into_static())) 83 | .collect(), 84 | ), 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Clone, PartialEq)] 90 | pub enum Type<'a, T: Text<'a>> { 91 | NamedType(T::Value), 92 | ListType(Box>), 93 | NonNullType(Box>), 94 | } 95 | 96 | impl Number { 97 | /// Returns a number as i64 if it fits the type 98 | pub fn as_i64(&self) -> Option { 99 | Some(self.0) 100 | } 101 | } 102 | 103 | impl From for Number { 104 | fn from(i: i32) -> Self { 105 | Number(i as i64) 106 | } 107 | } 108 | 109 | pub fn directives<'a, T>( 110 | input: &mut TokenStream<'a>, 111 | ) -> StdParseResult>, TokenStream<'a>> 112 | where 113 | T: Text<'a>, 114 | { 115 | many( 116 | position() 117 | .skip(punct("@")) 118 | .and(name::<'a, T>()) 119 | .and(parser(arguments)) 120 | .map(|((position, name), arguments)| Directive { 121 | position, 122 | name, 123 | arguments, 124 | }), 125 | ) 126 | .parse_stream(input) 127 | .into_result() 128 | } 129 | 130 | #[allow(clippy::type_complexity)] 131 | pub fn arguments<'a, T>( 132 | input: &mut TokenStream<'a>, 133 | ) -> StdParseResult)>, TokenStream<'a>> 134 | where 135 | T: Text<'a>, 136 | { 137 | optional( 138 | punct("(") 139 | .with(many1(name::<'a, T>().skip(punct(":")).and(parser(value)))) 140 | .skip(punct(")")), 141 | ) 142 | .map(|opt| opt.unwrap_or_default()) 143 | .parse_stream(input) 144 | .into_result() 145 | } 146 | 147 | pub fn int_value<'a, S>( 148 | input: &mut TokenStream<'a>, 149 | ) -> StdParseResult, TokenStream<'a>> 150 | where 151 | S: Text<'a>, 152 | { 153 | kind(T::IntValue) 154 | .and_then(|tok| tok.value.parse()) 155 | .map(Number) 156 | .map(Value::Int) 157 | .parse_stream(input) 158 | .into_result() 159 | } 160 | 161 | pub fn float_value<'a, S>( 162 | input: &mut TokenStream<'a>, 163 | ) -> StdParseResult, TokenStream<'a>> 164 | where 165 | S: Text<'a>, 166 | { 167 | kind(T::FloatValue) 168 | .and_then(|tok| tok.value.parse()) 169 | .map(Value::Float) 170 | .parse_stream(input) 171 | .into_result() 172 | } 173 | 174 | fn unquote_block_string(src: &str) -> Result, Token<'_>>> { 175 | debug_assert!(src.starts_with("\"\"\"") && src.ends_with("\"\"\"")); 176 | let lines = src[3..src.len() - 3].lines(); 177 | 178 | let mut common_indent = usize::MAX; 179 | let mut first_non_empty_line: Option = None; 180 | let mut last_non_empty_line = 0; 181 | for (idx, line) in lines.clone().enumerate() { 182 | let indent = line.len() - line.trim_start().len(); 183 | if indent == line.len() { 184 | continue; 185 | } 186 | 187 | first_non_empty_line.get_or_insert(idx); 188 | last_non_empty_line = idx; 189 | 190 | if idx != 0 { 191 | common_indent = std::cmp::min(common_indent, indent); 192 | } 193 | } 194 | 195 | if first_non_empty_line.is_none() { 196 | // The block string contains only whitespace. 197 | return Ok("".to_string()); 198 | } 199 | let first_non_empty_line = first_non_empty_line.unwrap(); 200 | 201 | let mut result = String::with_capacity(src.len() - 6); 202 | let mut lines = lines 203 | .enumerate() 204 | // Skip leading and trailing empty lines. 205 | .skip(first_non_empty_line) 206 | .take(last_non_empty_line - first_non_empty_line + 1) 207 | // Remove indent, except the first line. 208 | .map(|(idx, line)| { 209 | if idx != 0 && line.len() >= common_indent { 210 | &line[common_indent..] 211 | } else { 212 | line 213 | } 214 | }) 215 | // Handle escaped triple-quote (\"""). 216 | .map(|x| x.replace(r#"\""""#, r#"""""#)); 217 | 218 | if let Some(line) = lines.next() { 219 | result.push_str(&line); 220 | 221 | for line in lines { 222 | result.push('\n'); 223 | result.push_str(&line); 224 | } 225 | } 226 | Ok(result) 227 | } 228 | 229 | fn unquote_string(s: &str) -> Result> { 230 | let mut res = String::with_capacity(s.len()); 231 | debug_assert!(s.starts_with('"') && s.ends_with('"')); 232 | let mut chars = s[1..s.len() - 1].chars(); 233 | let mut temp_code_point = String::with_capacity(4); 234 | while let Some(c) = chars.next() { 235 | match c { 236 | '\\' => { 237 | match chars.next().expect("slash cant be at the end") { 238 | c @ '"' | c @ '\\' | c @ '/' => res.push(c), 239 | 'b' => res.push('\u{0010}'), 240 | 'f' => res.push('\u{000C}'), 241 | 'n' => res.push('\n'), 242 | 'r' => res.push('\r'), 243 | 't' => res.push('\t'), 244 | 'u' => { 245 | temp_code_point.clear(); 246 | for _ in 0..4 { 247 | match chars.next() { 248 | Some(inner_c) => temp_code_point.push(inner_c), 249 | None => { 250 | return Err(Error::Unexpected(Info::Owned( 251 | format_args!( 252 | "\\u must have 4 characters after it, only found '{}'", 253 | temp_code_point 254 | ) 255 | .to_string(), 256 | ))) 257 | } 258 | } 259 | } 260 | 261 | // convert our hex string into a u32, then convert that into a char 262 | match u32::from_str_radix(&temp_code_point, 16).map(std::char::from_u32) { 263 | Ok(Some(unicode_char)) => res.push(unicode_char), 264 | _ => { 265 | return Err(Error::Unexpected(Info::Owned( 266 | format_args!( 267 | "{} is not a valid unicode code point", 268 | temp_code_point 269 | ) 270 | .to_string(), 271 | ))) 272 | } 273 | } 274 | } 275 | c => { 276 | return Err(Error::Unexpected(Info::Owned( 277 | format_args!("bad escaped char {:?}", c).to_string(), 278 | ))); 279 | } 280 | } 281 | } 282 | c => res.push(c), 283 | } 284 | } 285 | 286 | Ok(res) 287 | } 288 | 289 | pub fn string<'a>(input: &mut TokenStream<'a>) -> StdParseResult> { 290 | choice(( 291 | kind(T::StringValue).and_then(|tok| unquote_string(tok.value)), 292 | kind(T::BlockString).and_then(|tok| unquote_block_string(tok.value)), 293 | )) 294 | .parse_stream(input) 295 | .into_result() 296 | } 297 | 298 | pub fn string_value<'a, S>( 299 | input: &mut TokenStream<'a>, 300 | ) -> StdParseResult, TokenStream<'a>> 301 | where 302 | S: Text<'a>, 303 | { 304 | kind(T::StringValue) 305 | .and_then(|tok| unquote_string(tok.value)) 306 | .map(Value::String) 307 | .parse_stream(input) 308 | .into_result() 309 | } 310 | 311 | pub fn block_string_value<'a, S>( 312 | input: &mut TokenStream<'a>, 313 | ) -> StdParseResult, TokenStream<'a>> 314 | where 315 | S: Text<'a>, 316 | { 317 | kind(T::BlockString) 318 | .and_then(|tok| unquote_block_string(tok.value)) 319 | .map(Value::String) 320 | .parse_stream(input) 321 | .into_result() 322 | } 323 | 324 | pub fn plain_value<'a, T>( 325 | input: &mut TokenStream<'a>, 326 | ) -> StdParseResult, TokenStream<'a>> 327 | where 328 | T: Text<'a>, 329 | { 330 | ident("true") 331 | .map(|_| Value::Boolean(true)) 332 | .or(ident("false").map(|_| Value::Boolean(false))) 333 | .or(ident("null").map(|_| Value::Null)) 334 | .or(name::<'a, T>().map(Value::Enum)) 335 | .or(parser(int_value)) 336 | .or(parser(float_value)) 337 | .or(parser(string_value)) 338 | .or(parser(block_string_value)) 339 | .parse_stream(input) 340 | .into_result() 341 | } 342 | 343 | pub fn value<'a, T>(input: &mut TokenStream<'a>) -> StdParseResult, TokenStream<'a>> 344 | where 345 | T: Text<'a>, 346 | { 347 | parser(plain_value) 348 | .or(punct("$").with(name::<'a, T>()).map(Value::Variable)) 349 | .or(punct("[") 350 | .with(many(parser(value))) 351 | .skip(punct("]")) 352 | .map(Value::List)) 353 | .or(punct("{") 354 | .with(many(name::<'a, T>().skip(punct(":")).and(parser(value)))) 355 | .skip(punct("}")) 356 | .map(Value::Object)) 357 | .parse_stream(input) 358 | .into_result() 359 | } 360 | 361 | pub fn default_value<'a, T>( 362 | input: &mut TokenStream<'a>, 363 | ) -> StdParseResult, TokenStream<'a>> 364 | where 365 | T: Text<'a>, 366 | { 367 | parser(plain_value) 368 | .or(punct("[") 369 | .with(many(parser(default_value))) 370 | .skip(punct("]")) 371 | .map(Value::List)) 372 | .or(punct("{") 373 | .with(many( 374 | name::<'a, T>().skip(punct(":")).and(parser(default_value)), 375 | )) 376 | .skip(punct("}")) 377 | .map(Value::Object)) 378 | .parse_stream(input) 379 | .into_result() 380 | } 381 | 382 | pub fn parse_type<'a, T>( 383 | input: &mut TokenStream<'a>, 384 | ) -> StdParseResult, TokenStream<'a>> 385 | where 386 | T: Text<'a>, 387 | { 388 | name::<'a, T>() 389 | .map(Type::NamedType) 390 | .or(punct("[") 391 | .with(parser(parse_type)) 392 | .skip(punct("]")) 393 | .map(Box::new) 394 | .map(Type::ListType)) 395 | .and(optional(punct("!")).map(|v| v.is_some())) 396 | .map(|(typ, strict)| { 397 | if strict { 398 | Type::NonNullType(Box::new(typ)) 399 | } else { 400 | typ 401 | } 402 | }) 403 | .parse_stream(input) 404 | .into_result() 405 | } 406 | 407 | #[cfg(test)] 408 | mod tests { 409 | use super::unquote_block_string; 410 | use super::unquote_string; 411 | use super::Number; 412 | 413 | #[test] 414 | fn number_from_i32_and_to_i64_conversion() { 415 | assert_eq!(Number::from(1).as_i64(), Some(1)); 416 | assert_eq!(Number::from(584).as_i64(), Some(584)); 417 | assert_eq!(Number::from(i32::MIN).as_i64(), Some(i32::MIN as i64)); 418 | assert_eq!(Number::from(i32::MAX).as_i64(), Some(i32::MAX as i64)); 419 | } 420 | 421 | #[test] 422 | fn unquote_unicode_string() { 423 | // basic tests 424 | assert_eq!(unquote_string(r#""\u0009""#).expect(""), "\u{0009}"); 425 | assert_eq!(unquote_string(r#""\u000A""#).expect(""), "\u{000A}"); 426 | assert_eq!(unquote_string(r#""\u000D""#).expect(""), "\u{000D}"); 427 | assert_eq!(unquote_string(r#""\u0020""#).expect(""), "\u{0020}"); 428 | assert_eq!(unquote_string(r#""\uFFFF""#).expect(""), "\u{FFFF}"); 429 | 430 | // a more complex string 431 | assert_eq!( 432 | unquote_string(r#""\u0009 hello \u000A there""#).expect(""), 433 | "\u{0009} hello \u{000A} there" 434 | ); 435 | } 436 | 437 | #[test] 438 | fn block_string_leading_and_trailing_empty_lines() { 439 | let block = &triple_quote(" \n\n Hello,\n World!\n\n Yours,\n GraphQL.\n\n\n"); 440 | assert_eq!( 441 | unquote_block_string(block), 442 | Result::Ok("Hello,\n World!\n\nYours,\n GraphQL.".to_string()) 443 | ); 444 | } 445 | 446 | #[test] 447 | fn block_string_indent() { 448 | let block = &triple_quote("Hello \n\n Hello,\n World!\n"); 449 | assert_eq!( 450 | unquote_block_string(block), 451 | Result::Ok("Hello \n\nHello,\n World!".to_string()) 452 | ); 453 | } 454 | 455 | #[test] 456 | fn block_string_escaping() { 457 | let block = triple_quote(r#"\""""#); 458 | assert_eq!( 459 | unquote_block_string(&block), 460 | Result::Ok("\"\"\"".to_string()) 461 | ); 462 | } 463 | 464 | #[test] 465 | fn block_string_empty() { 466 | let block = triple_quote(""); 467 | assert_eq!(unquote_block_string(&block), Result::Ok("".to_string())); 468 | let block = triple_quote(" \n\t\n"); 469 | assert_eq!(unquote_block_string(&block), Result::Ok("".to_string())); 470 | } 471 | 472 | fn triple_quote(input: &str) -> String { 473 | format!("\"\"\"{}\"\"\"", input) 474 | } 475 | } 476 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | //! Formatting graphql 2 | use std::default::Default; 3 | 4 | use crate::common::Directive; 5 | 6 | #[derive(Debug, PartialEq)] 7 | pub(crate) struct Formatter<'a> { 8 | buf: String, 9 | style: &'a Style, 10 | indent: u32, 11 | } 12 | 13 | /// A configuration of formatting style 14 | /// 15 | /// Currently we only have indentation configured, other things might be 16 | /// added later (such as minification). 17 | #[derive(Debug, PartialEq, Clone)] 18 | pub struct Style { 19 | indent: u32, 20 | multiline_arguments: bool, 21 | } 22 | 23 | impl Default for Style { 24 | fn default() -> Style { 25 | Style { 26 | indent: 2, 27 | multiline_arguments: false, 28 | } 29 | } 30 | } 31 | 32 | impl Style { 33 | /// Change the number of spaces used for indentation 34 | pub fn indent(&mut self, indent: u32) -> &mut Self { 35 | self.indent = indent; 36 | self 37 | } 38 | 39 | /// Set whether to add new lines between arguments 40 | pub fn multiline_arguments(&mut self, multiline_arguments: bool) -> &mut Self { 41 | self.multiline_arguments = multiline_arguments; 42 | self 43 | } 44 | } 45 | 46 | pub(crate) trait Displayable { 47 | fn display(&self, f: &mut Formatter); 48 | } 49 | 50 | impl<'a> Formatter<'a> { 51 | pub fn new(style: &Style) -> Formatter { 52 | Formatter { 53 | buf: String::with_capacity(1024), 54 | style, 55 | indent: 0, 56 | } 57 | } 58 | 59 | pub fn indent(&mut self) { 60 | for _ in 0..self.indent { 61 | self.buf.push(' '); 62 | } 63 | } 64 | 65 | pub fn endline(&mut self) { 66 | self.buf.push('\n'); 67 | } 68 | 69 | pub fn start_argument_block(&mut self, open_char: char) { 70 | self.buf.push(open_char); 71 | if self.style.multiline_arguments { 72 | self.inc_indent(); 73 | } 74 | } 75 | 76 | pub fn end_argument_block(&mut self, close_char: char) { 77 | if self.style.multiline_arguments { 78 | self.endline(); 79 | self.dec_indent(); 80 | self.indent(); 81 | } 82 | self.buf.push(close_char); 83 | } 84 | 85 | pub fn start_argument(&mut self) { 86 | if self.style.multiline_arguments { 87 | self.endline(); 88 | self.indent(); 89 | } 90 | } 91 | 92 | pub fn deliniate_argument(&mut self) { 93 | self.buf.push(','); 94 | if !self.style.multiline_arguments { 95 | self.buf.push(' '); 96 | } 97 | } 98 | 99 | pub fn start_block(&mut self) { 100 | self.buf.push('{'); 101 | self.endline(); 102 | self.inc_indent(); 103 | } 104 | 105 | pub fn end_block(&mut self) { 106 | self.dec_indent(); 107 | self.indent(); 108 | self.buf.push('}'); 109 | self.endline(); 110 | } 111 | 112 | pub fn margin(&mut self) { 113 | if !self.buf.is_empty() { 114 | self.buf.push('\n'); 115 | } 116 | } 117 | 118 | pub fn write(&mut self, s: &str) { 119 | self.buf.push_str(s); 120 | } 121 | 122 | pub fn into_string(self) -> String { 123 | self.buf 124 | } 125 | 126 | pub fn write_quoted(&mut self, s: &str) { 127 | let mut has_newline = false; 128 | let mut has_nonprintable = false; 129 | for c in s.chars() { 130 | match c { 131 | '\n' => has_newline = true, 132 | '\r' | '\t' | '\u{0020}'..='\u{FFFF}' => {} 133 | _ => has_nonprintable = true, 134 | } 135 | } 136 | if !has_newline || has_nonprintable { 137 | use std::fmt::Write; 138 | self.buf.push('"'); 139 | for c in s.chars() { 140 | match c { 141 | '\r' => self.write(r"\r"), 142 | '\n' => self.write(r"\n"), 143 | '\t' => self.write(r"\t"), 144 | '"' => self.write("\\\""), 145 | '\\' => self.write(r"\\"), 146 | '\u{0020}'..='\u{FFFF}' => self.buf.push(c), 147 | _ => write!(&mut self.buf, "\\u{:04}", c as u32).unwrap(), 148 | } 149 | } 150 | self.buf.push('"'); 151 | } else { 152 | self.buf.push_str(r#"""""#); 153 | self.endline(); 154 | self.indent += self.style.indent; 155 | for line in s.lines() { 156 | if !line.trim().is_empty() { 157 | self.indent(); 158 | self.write(&line.replace(r#"""""#, r#"\""""#)); 159 | } 160 | self.endline(); 161 | } 162 | self.indent -= self.style.indent; 163 | self.indent(); 164 | self.buf.push_str(r#"""""#); 165 | } 166 | } 167 | 168 | fn inc_indent(&mut self) { 169 | self.indent += self.style.indent; 170 | } 171 | 172 | fn dec_indent(&mut self) { 173 | self.indent = self 174 | .indent 175 | .checked_sub(self.style.indent) 176 | .expect("negative indent"); 177 | } 178 | } 179 | 180 | pub(crate) fn format_directives<'a, T>(dirs: &[Directive<'a, T>], f: &mut Formatter) 181 | where 182 | T: crate::common::Text<'a>, 183 | { 184 | for dir in dirs { 185 | f.write(" "); 186 | dir.display(f); 187 | } 188 | } 189 | 190 | macro_rules! impl_display { 191 | ($( $typ: ident, )+) => { 192 | $( 193 | impl fmt::Display for $typ { 194 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 195 | f.write_str(&to_string(self)) 196 | } 197 | } 198 | )+ 199 | }; 200 | 201 | ('a $($typ: ident, )+) => { 202 | $( 203 | impl<'a, T> fmt::Display for $typ<'a, T> 204 | where T: Text<'a>, 205 | { 206 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 207 | f.write_str(&to_string(self)) 208 | } 209 | } 210 | )+ 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use combine::error::Tracked; 4 | use combine::stream::easy::{Error, Errors, Info}; 5 | use combine::{satisfy, ParseResult, Parser, StreamOnce}; 6 | 7 | use crate::position::Pos; 8 | use crate::tokenizer::{Kind, Token, TokenStream}; 9 | 10 | use super::common::Text; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct TokenMatch<'a> { 14 | kind: Kind, 15 | phantom: PhantomData<&'a u8>, 16 | } 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct NameMatch<'a, T> 20 | where 21 | T: Text<'a>, 22 | { 23 | phantom: PhantomData<&'a T>, 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct Value<'a> { 28 | kind: Kind, 29 | value: &'static str, 30 | phantom: PhantomData<&'a u8>, 31 | } 32 | 33 | pub fn kind<'x>(kind: Kind) -> TokenMatch<'x> { 34 | TokenMatch { 35 | kind, 36 | phantom: PhantomData, 37 | } 38 | } 39 | 40 | pub fn name<'a, T>() -> NameMatch<'a, T> 41 | where 42 | T: Text<'a>, 43 | { 44 | NameMatch { 45 | phantom: PhantomData, 46 | } 47 | } 48 | 49 | impl<'a> Parser> for TokenMatch<'a> { 50 | type Output = Token<'a>; 51 | type PartialState = (); 52 | 53 | #[inline] 54 | fn parse_lazy( 55 | &mut self, 56 | input: &mut TokenStream<'a>, 57 | ) -> ParseResult as StreamOnce>::Error> { 58 | satisfy(|c: Token<'a>| c.kind == self.kind).parse_lazy(input) 59 | } 60 | 61 | fn add_error(&mut self, error: &mut Tracked, Token<'a>, Pos>>) { 62 | error 63 | .error 64 | .add_error(Error::Expected(Info::Owned(format!("{:?}", self.kind)))); 65 | } 66 | } 67 | 68 | pub fn punct<'s>(value: &'static str) -> Value<'s> { 69 | Value { 70 | kind: Kind::Punctuator, 71 | value, 72 | phantom: PhantomData, 73 | } 74 | } 75 | 76 | pub fn ident<'s>(value: &'static str) -> Value<'s> { 77 | Value { 78 | kind: Kind::Name, 79 | value, 80 | phantom: PhantomData, 81 | } 82 | } 83 | 84 | impl<'a> Parser> for Value<'a> { 85 | type Output = Token<'a>; 86 | type PartialState = (); 87 | 88 | #[inline] 89 | fn parse_lazy( 90 | &mut self, 91 | input: &mut TokenStream<'a>, 92 | ) -> ParseResult as StreamOnce>::Error> { 93 | satisfy(|c: Token<'a>| c.kind == self.kind && c.value == self.value).parse_lazy(input) 94 | } 95 | 96 | fn add_error(&mut self, error: &mut Tracked< as StreamOnce>::Error>) { 97 | error 98 | .error 99 | .add_error(Error::Expected(Info::Static(self.value))); 100 | } 101 | } 102 | 103 | impl<'a, S> Parser> for NameMatch<'a, S> 104 | where 105 | S: Text<'a>, 106 | { 107 | type Output = S::Value; 108 | type PartialState = (); 109 | 110 | #[inline] 111 | fn parse_lazy( 112 | &mut self, 113 | input: &mut TokenStream<'a>, 114 | ) -> ParseResult as StreamOnce>::Error> { 115 | satisfy(|c: Token<'a>| c.kind == Kind::Name) 116 | .map(|t: Token<'a>| -> S::Value { S::Value::from(t.value) }) 117 | .parse_lazy(input) 118 | } 119 | 120 | fn add_error(&mut self, error: &mut Tracked, Token<'a>, Pos>>) { 121 | error.error.add_error(Error::Expected(Info::Static("Name"))); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Graphql Parser 2 | //! ============== 3 | //! 4 | //! This library contains full parser and formatter of the graphql 5 | //! query language as well as AST types. 6 | //! 7 | //! [Docs](https://docs.rs/graphql-parser/) | 8 | //! [Github](https://github.com/graphql-rust/graphql-parser/) | 9 | //! [Crate](https://crates.io/crates/graphql-parser) 10 | //! 11 | //! Current this library supports full graphql syntax, and the following 12 | //! extensions: 13 | //! 14 | //! 1. Subscriptions 15 | //! 2. Block (triple quoted) strings 16 | //! 3. Schema definition language a/k/a IDL (which is still in RFC) 17 | //! 18 | //! 19 | //! Example: Parse and Format Query 20 | //! ------------------------------- 21 | //! 22 | //! ```rust 23 | //! # extern crate graphql_parser; 24 | //! use graphql_parser::query::{parse_query, ParseError}; 25 | //! 26 | //! # fn parse() -> Result<(), ParseError> { 27 | //! let ast = parse_query::<&str>("query MyQuery { field1, field2 }")?; 28 | //! // Format canonical representation 29 | //! assert_eq!(format!("{}", ast), "\ 30 | //! query MyQuery { 31 | //! field1 32 | //! field2 33 | //! } 34 | //! "); 35 | //! # Ok(()) 36 | //! # } 37 | //! # fn main() { 38 | //! # parse().unwrap() 39 | //! # } 40 | //! ``` 41 | //! 42 | //! Example: Parse and Format Schema 43 | //! -------------------------------- 44 | //! 45 | //! ```rust 46 | //! # extern crate graphql_parser; 47 | //! use graphql_parser::schema::{parse_schema, ParseError}; 48 | //! 49 | //! # fn parse() -> Result<(), ParseError> { 50 | //! let ast = parse_schema::(r#" 51 | //! schema { 52 | //! query: Query 53 | //! } 54 | //! type Query { 55 | //! users: [User!]!, 56 | //! } 57 | //! """ 58 | //! Example user object 59 | //! 60 | //! This is just a demo comment. 61 | //! """ 62 | //! type User { 63 | //! name: String!, 64 | //! } 65 | //! "#)?.to_owned(); 66 | //! // Format canonical representation 67 | //! assert_eq!(format!("{}", ast), "\ 68 | //! schema { 69 | //! query: Query 70 | //! } 71 | //! 72 | //! type Query { 73 | //! users: [User!]! 74 | //! } 75 | //! 76 | //! \"\"\" 77 | //! Example user object 78 | //! 79 | //! This is just a demo comment. 80 | //! \"\"\" 81 | //! type User { 82 | //! name: String! 83 | //! } 84 | //! "); 85 | //! # Ok(()) 86 | //! # } 87 | //! # fn main() { 88 | //! # parse().unwrap() 89 | //! # } 90 | //! ``` 91 | //! 92 | #![warn(missing_debug_implementations)] 93 | 94 | #[cfg(test)] 95 | #[macro_use] 96 | extern crate pretty_assertions; 97 | 98 | mod common; 99 | #[macro_use] 100 | mod format; 101 | mod helpers; 102 | mod position; 103 | pub mod query; 104 | pub mod schema; 105 | mod tokenizer; 106 | 107 | pub use crate::format::Style; 108 | pub use crate::position::Pos; 109 | pub use crate::query::minify_query; 110 | pub use crate::query::parse_query; 111 | pub use crate::schema::parse_schema; 112 | -------------------------------------------------------------------------------- /src/position.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// Original position of element in source code 4 | #[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Default, Hash)] 5 | pub struct Pos { 6 | /// One-based line number 7 | pub line: usize, 8 | /// One-based column number 9 | pub column: usize, 10 | } 11 | 12 | impl fmt::Debug for Pos { 13 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | write!(f, "Pos({}:{})", self.line, self.column) 15 | } 16 | } 17 | 18 | impl fmt::Display for Pos { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | write!(f, "{}:{}", self.line, self.column) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/query/ast.rs: -------------------------------------------------------------------------------- 1 | //! Query Language Abstract Syntax Tree (AST) 2 | //! 3 | //! The types and fields here resemble official [graphql grammar] whenever it 4 | //! makes sense for rust. 5 | //! 6 | //! [graphql grammar]: http://facebook.github.io/graphql/October2016/#sec-Appendix-Grammar-Summary 7 | //! 8 | pub use crate::common::{Directive, Number, Text, Type, Value}; 9 | use crate::position::Pos; 10 | 11 | /// Root of query data 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub struct Document<'a, T: Text<'a>> { 14 | pub definitions: Vec>, 15 | } 16 | 17 | impl<'a> Document<'a, String> { 18 | pub fn into_static(self) -> Document<'static, String> { 19 | // To support both reference and owned values in the AST, 20 | // all string data is represented with the ::common::Str<'a, T: Text<'a>> 21 | // wrapper type. 22 | // This type must carry the lifetime of the query string, 23 | // and is stored in a PhantomData value on the Str type. 24 | // When using owned String types, the actual lifetime of 25 | // the Ast nodes is 'static, since no references are kept, 26 | // but the nodes will still carry the input lifetime. 27 | // To continue working with Document in a owned fasion 28 | // the lifetime needs to be transmuted to 'static. 29 | // 30 | // This is safe because no references are present. 31 | // Just the PhantomData lifetime reference is transmuted away. 32 | unsafe { std::mem::transmute::<_, Document<'static, String>>(self) } 33 | } 34 | } 35 | 36 | #[derive(Debug, Clone, PartialEq)] 37 | pub enum Definition<'a, T: Text<'a>> { 38 | Operation(OperationDefinition<'a, T>), 39 | Fragment(FragmentDefinition<'a, T>), 40 | } 41 | 42 | #[derive(Debug, Clone, PartialEq)] 43 | pub struct FragmentDefinition<'a, T: Text<'a>> { 44 | pub position: Pos, 45 | pub name: T::Value, 46 | pub type_condition: TypeCondition<'a, T>, 47 | pub directives: Vec>, 48 | pub selection_set: SelectionSet<'a, T>, 49 | } 50 | 51 | #[derive(Debug, Clone, PartialEq)] 52 | pub enum OperationDefinition<'a, T: Text<'a>> { 53 | SelectionSet(SelectionSet<'a, T>), 54 | Query(Query<'a, T>), 55 | Mutation(Mutation<'a, T>), 56 | Subscription(Subscription<'a, T>), 57 | } 58 | 59 | #[derive(Debug, Clone, PartialEq)] 60 | pub struct Query<'a, T: Text<'a>> { 61 | pub position: Pos, 62 | pub name: Option, 63 | pub variable_definitions: Vec>, 64 | pub directives: Vec>, 65 | pub selection_set: SelectionSet<'a, T>, 66 | } 67 | 68 | #[derive(Debug, Clone, PartialEq)] 69 | pub struct Mutation<'a, T: Text<'a>> { 70 | pub position: Pos, 71 | pub name: Option, 72 | pub variable_definitions: Vec>, 73 | pub directives: Vec>, 74 | pub selection_set: SelectionSet<'a, T>, 75 | } 76 | 77 | #[derive(Debug, Clone, PartialEq)] 78 | pub struct Subscription<'a, T: Text<'a>> { 79 | pub position: Pos, 80 | pub name: Option, 81 | pub variable_definitions: Vec>, 82 | pub directives: Vec>, 83 | pub selection_set: SelectionSet<'a, T>, 84 | } 85 | 86 | #[derive(Debug, Clone, PartialEq)] 87 | pub struct SelectionSet<'a, T: Text<'a>> { 88 | pub span: (Pos, Pos), 89 | pub items: Vec>, 90 | } 91 | 92 | #[derive(Debug, Clone, PartialEq)] 93 | pub struct VariableDefinition<'a, T: Text<'a>> { 94 | pub position: Pos, 95 | pub name: T::Value, 96 | pub var_type: Type<'a, T>, 97 | pub default_value: Option>, 98 | } 99 | 100 | #[derive(Debug, Clone, PartialEq)] 101 | pub enum Selection<'a, T: Text<'a>> { 102 | Field(Field<'a, T>), 103 | FragmentSpread(FragmentSpread<'a, T>), 104 | InlineFragment(InlineFragment<'a, T>), 105 | } 106 | 107 | #[derive(Debug, Clone, PartialEq)] 108 | pub struct Field<'a, T: Text<'a>> { 109 | pub position: Pos, 110 | pub alias: Option, 111 | pub name: T::Value, 112 | pub arguments: Vec<(T::Value, Value<'a, T>)>, 113 | pub directives: Vec>, 114 | pub selection_set: SelectionSet<'a, T>, 115 | } 116 | 117 | #[derive(Debug, Clone, PartialEq)] 118 | pub struct FragmentSpread<'a, T: Text<'a>> { 119 | pub position: Pos, 120 | pub fragment_name: T::Value, 121 | pub directives: Vec>, 122 | } 123 | 124 | #[derive(Debug, Clone, PartialEq)] 125 | pub enum TypeCondition<'a, T: Text<'a>> { 126 | On(T::Value), 127 | } 128 | 129 | #[derive(Debug, Clone, PartialEq)] 130 | pub struct InlineFragment<'a, T: Text<'a>> { 131 | pub position: Pos, 132 | pub type_condition: Option>, 133 | pub directives: Vec>, 134 | pub selection_set: SelectionSet<'a, T>, 135 | } 136 | -------------------------------------------------------------------------------- /src/query/error.rs: -------------------------------------------------------------------------------- 1 | use combine::easy::Errors; 2 | use thiserror::Error; 3 | 4 | use crate::position::Pos; 5 | use crate::tokenizer::Token; 6 | 7 | pub type InternalError<'a> = Errors, Token<'a>, Pos>; 8 | 9 | /// Error parsing query 10 | /// 11 | /// This structure is opaque for forward compatibility. We are exploring a 12 | /// way to improve both error message and API. 13 | #[derive(Error, Debug)] 14 | #[error("query parse error: {}", _0)] 15 | pub struct ParseError(String); 16 | 17 | impl<'a> From> for ParseError { 18 | fn from(e: InternalError<'a>) -> ParseError { 19 | ParseError(format!("{}", e)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/query/format.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::format::{format_directives, Displayable, Formatter, Style}; 4 | 5 | use crate::query::ast::*; 6 | 7 | impl<'a, T: Text<'a>> Document<'a, T> { 8 | /// Format a document according to style 9 | pub fn format(&self, style: &Style) -> String { 10 | let mut formatter = Formatter::new(style); 11 | self.display(&mut formatter); 12 | formatter.into_string() 13 | } 14 | } 15 | 16 | fn to_string(v: &T) -> String { 17 | let style = Style::default(); 18 | let mut formatter = Formatter::new(&style); 19 | v.display(&mut formatter); 20 | formatter.into_string() 21 | } 22 | 23 | impl<'a, T: Text<'a>> Displayable for Document<'a, T> { 24 | fn display(&self, f: &mut Formatter) { 25 | for item in &self.definitions { 26 | item.display(f); 27 | } 28 | } 29 | } 30 | 31 | impl<'a, T: Text<'a>> Displayable for Definition<'a, T> { 32 | fn display(&self, f: &mut Formatter) { 33 | match *self { 34 | Definition::Operation(ref op) => op.display(f), 35 | Definition::Fragment(ref frag) => frag.display(f), 36 | } 37 | } 38 | } 39 | 40 | impl<'a, T: Text<'a>> Displayable for OperationDefinition<'a, T> { 41 | fn display(&self, f: &mut Formatter) { 42 | match *self { 43 | OperationDefinition::SelectionSet(ref set) => set.display(f), 44 | OperationDefinition::Query(ref q) => q.display(f), 45 | OperationDefinition::Mutation(ref m) => m.display(f), 46 | OperationDefinition::Subscription(ref s) => s.display(f), 47 | } 48 | } 49 | } 50 | 51 | impl<'a, T: Text<'a>> Displayable for FragmentDefinition<'a, T> { 52 | fn display(&self, f: &mut Formatter) { 53 | f.margin(); 54 | f.indent(); 55 | f.write("fragment "); 56 | f.write(self.name.as_ref()); 57 | f.write(" "); 58 | self.type_condition.display(f); 59 | format_directives(&self.directives, f); 60 | f.write(" "); 61 | f.start_block(); 62 | for item in &self.selection_set.items { 63 | item.display(f); 64 | } 65 | f.end_block(); 66 | } 67 | } 68 | 69 | impl<'a, T: Text<'a>> Displayable for SelectionSet<'a, T> { 70 | fn display(&self, f: &mut Formatter) { 71 | f.margin(); 72 | f.indent(); 73 | f.start_block(); 74 | for item in &self.items { 75 | item.display(f); 76 | } 77 | f.end_block(); 78 | } 79 | } 80 | 81 | impl<'a, T: Text<'a>> Displayable for Selection<'a, T> { 82 | fn display(&self, f: &mut Formatter) { 83 | match *self { 84 | Selection::Field(ref fld) => fld.display(f), 85 | Selection::InlineFragment(ref frag) => frag.display(f), 86 | Selection::FragmentSpread(ref frag) => frag.display(f), 87 | } 88 | } 89 | } 90 | 91 | fn format_arguments<'a, T: Text<'a>>(arguments: &[(T::Value, Value<'a, T>)], f: &mut Formatter) { 92 | if !arguments.is_empty() { 93 | f.start_argument_block('('); 94 | f.start_argument(); 95 | f.write(arguments[0].0.as_ref()); 96 | f.write(": "); 97 | arguments[0].1.display(f); 98 | for arg in &arguments[1..] { 99 | f.deliniate_argument(); 100 | f.start_argument(); 101 | f.write(arg.0.as_ref()); 102 | f.write(": "); 103 | arg.1.display(f); 104 | } 105 | f.end_argument_block(')'); 106 | } 107 | } 108 | 109 | impl<'a, T: Text<'a>> Displayable for Field<'a, T> { 110 | fn display(&self, f: &mut Formatter) { 111 | f.indent(); 112 | if let Some(ref alias) = self.alias { 113 | f.write(alias.as_ref()); 114 | f.write(": "); 115 | } 116 | f.write(self.name.as_ref()); 117 | format_arguments(&self.arguments, f); 118 | format_directives(&self.directives, f); 119 | if !self.selection_set.items.is_empty() { 120 | f.write(" "); 121 | f.start_block(); 122 | for item in &self.selection_set.items { 123 | item.display(f); 124 | } 125 | f.end_block(); 126 | } else { 127 | f.endline(); 128 | } 129 | } 130 | } 131 | 132 | impl<'a, T: Text<'a>> Displayable for Query<'a, T> { 133 | fn display(&self, f: &mut Formatter) { 134 | f.margin(); 135 | f.indent(); 136 | f.write("query"); 137 | if let Some(ref name) = self.name { 138 | f.write(" "); 139 | f.write(name.as_ref()); 140 | } 141 | if !self.variable_definitions.is_empty() { 142 | f.write("("); 143 | self.variable_definitions[0].display(f); 144 | for var in &self.variable_definitions[1..] { 145 | f.write(", "); 146 | var.display(f); 147 | } 148 | f.write(")"); 149 | } 150 | format_directives(&self.directives, f); 151 | f.write(" "); 152 | f.start_block(); 153 | for item in &self.selection_set.items { 154 | item.display(f); 155 | } 156 | f.end_block(); 157 | } 158 | } 159 | 160 | impl<'a, T: Text<'a>> Displayable for Mutation<'a, T> { 161 | fn display(&self, f: &mut Formatter) { 162 | f.margin(); 163 | f.indent(); 164 | f.write("mutation"); 165 | if let Some(ref name) = self.name { 166 | f.write(" "); 167 | f.write(name.as_ref()); 168 | } 169 | if !self.variable_definitions.is_empty() { 170 | f.write("("); 171 | self.variable_definitions[0].display(f); 172 | for var in &self.variable_definitions[1..] { 173 | f.write(", "); 174 | var.display(f); 175 | } 176 | f.write(")"); 177 | } 178 | format_directives(&self.directives, f); 179 | f.write(" "); 180 | f.start_block(); 181 | for item in &self.selection_set.items { 182 | item.display(f); 183 | } 184 | f.end_block(); 185 | } 186 | } 187 | 188 | impl<'a, T: Text<'a>> Displayable for Subscription<'a, T> { 189 | fn display(&self, f: &mut Formatter) { 190 | f.margin(); 191 | f.indent(); 192 | f.write("subscription"); 193 | if let Some(ref name) = self.name { 194 | f.write(" "); 195 | f.write(name.as_ref()); 196 | if !self.variable_definitions.is_empty() { 197 | f.write("("); 198 | for var in &self.variable_definitions { 199 | var.display(f); 200 | } 201 | f.write(")"); 202 | } 203 | } 204 | format_directives(&self.directives, f); 205 | f.write(" "); 206 | f.start_block(); 207 | for item in &self.selection_set.items { 208 | item.display(f); 209 | } 210 | f.end_block(); 211 | } 212 | } 213 | 214 | impl<'a, T: Text<'a>> Displayable for VariableDefinition<'a, T> { 215 | fn display(&self, f: &mut Formatter) { 216 | f.write("$"); 217 | f.write(self.name.as_ref()); 218 | f.write(": "); 219 | self.var_type.display(f); 220 | if let Some(ref default) = self.default_value { 221 | f.write(" = "); 222 | default.display(f); 223 | } 224 | } 225 | } 226 | 227 | impl<'a, T: Text<'a>> Displayable for Type<'a, T> { 228 | fn display(&self, f: &mut Formatter) { 229 | match *self { 230 | Type::NamedType(ref name) => f.write(name.as_ref()), 231 | Type::ListType(ref typ) => { 232 | f.write("["); 233 | typ.display(f); 234 | f.write("]"); 235 | } 236 | Type::NonNullType(ref typ) => { 237 | typ.display(f); 238 | f.write("!"); 239 | } 240 | } 241 | } 242 | } 243 | 244 | impl<'a, T: Text<'a>> Displayable for Value<'a, T> { 245 | fn display(&self, f: &mut Formatter) { 246 | match *self { 247 | Value::Variable(ref name) => { 248 | f.write("$"); 249 | f.write(name.as_ref()); 250 | } 251 | Value::Int(ref num) => f.write(&format!("{}", num.0)), 252 | Value::Float(val) => f.write(&format!("{}", val)), 253 | Value::String(ref val) => f.write_quoted(val), 254 | Value::Boolean(true) => f.write("true"), 255 | Value::Boolean(false) => f.write("false"), 256 | Value::Null => f.write("null"), 257 | Value::Enum(ref name) => f.write(name.as_ref()), 258 | Value::List(ref items) => { 259 | f.start_argument_block('['); 260 | if !items.is_empty() { 261 | f.start_argument(); 262 | items[0].display(f); 263 | for item in &items[1..] { 264 | f.deliniate_argument(); 265 | f.start_argument(); 266 | item.display(f); 267 | } 268 | } 269 | f.end_argument_block(']'); 270 | } 271 | Value::Object(ref items) => { 272 | f.start_argument_block('{'); 273 | let mut first = true; 274 | for (name, value) in items.iter() { 275 | if first { 276 | first = false; 277 | } else { 278 | f.deliniate_argument(); 279 | } 280 | f.start_argument(); 281 | f.write(name.as_ref()); 282 | f.write(": "); 283 | value.display(f); 284 | } 285 | f.end_argument_block('}'); 286 | } 287 | } 288 | } 289 | } 290 | 291 | impl<'a, T: Text<'a>> Displayable for InlineFragment<'a, T> { 292 | fn display(&self, f: &mut Formatter) { 293 | f.indent(); 294 | f.write("..."); 295 | if let Some(ref cond) = self.type_condition { 296 | f.write(" "); 297 | cond.display(f); 298 | } 299 | format_directives(&self.directives, f); 300 | f.write(" "); 301 | f.start_block(); 302 | for item in &self.selection_set.items { 303 | item.display(f); 304 | } 305 | f.end_block(); 306 | } 307 | } 308 | 309 | impl<'a, T: Text<'a>> Displayable for TypeCondition<'a, T> { 310 | fn display(&self, f: &mut Formatter) { 311 | match *self { 312 | TypeCondition::On(ref name) => { 313 | f.write("on "); 314 | f.write(name.as_ref()); 315 | } 316 | } 317 | } 318 | } 319 | 320 | impl<'a, T: Text<'a>> Displayable for FragmentSpread<'a, T> { 321 | fn display(&self, f: &mut Formatter) { 322 | f.indent(); 323 | f.write("..."); 324 | f.write(self.fragment_name.as_ref()); 325 | format_directives(&self.directives, f); 326 | f.endline(); 327 | } 328 | } 329 | 330 | impl<'a, T: Text<'a>> Displayable for Directive<'a, T> { 331 | fn display(&self, f: &mut Formatter) { 332 | f.write("@"); 333 | f.write(self.name.as_ref()); 334 | format_arguments(self.arguments.as_slice(), f); 335 | } 336 | } 337 | 338 | impl_display!( 339 | 'a 340 | Document, 341 | Definition, 342 | OperationDefinition, 343 | FragmentDefinition, 344 | SelectionSet, 345 | Field, 346 | Query, 347 | Mutation, 348 | Subscription, 349 | VariableDefinition, 350 | Type, 351 | Value, 352 | InlineFragment, 353 | TypeCondition, 354 | FragmentSpread, 355 | Directive, 356 | ); 357 | -------------------------------------------------------------------------------- /src/query/grammar.rs: -------------------------------------------------------------------------------- 1 | use combine::{eof, many1, optional, position, StdParseResult}; 2 | use combine::{parser, Parser}; 3 | 4 | use crate::common::Directive; 5 | use crate::common::{arguments, default_value, directives, parse_type}; 6 | use crate::helpers::{ident, name, punct}; 7 | use crate::query::ast::*; 8 | use crate::query::error::ParseError; 9 | use crate::tokenizer::TokenStream; 10 | 11 | pub fn field<'a, S>(input: &mut TokenStream<'a>) -> StdParseResult, TokenStream<'a>> 12 | where 13 | S: Text<'a>, 14 | { 15 | ( 16 | position(), 17 | name::<'a, S>(), 18 | optional(punct(":").with(name::<'a, S>())), 19 | parser(arguments), 20 | parser(directives), 21 | optional(parser(selection_set)), 22 | ) 23 | .map( 24 | |(position, name_or_alias, opt_name, arguments, directives, sel)| { 25 | let (name, alias) = match opt_name { 26 | Some(name) => (name, Some(name_or_alias)), 27 | None => (name_or_alias, None), 28 | }; 29 | Field { 30 | position, 31 | name, 32 | alias, 33 | arguments, 34 | directives, 35 | selection_set: sel.unwrap_or_else(|| SelectionSet { 36 | span: (position, position), 37 | items: Vec::new(), 38 | }), 39 | } 40 | }, 41 | ) 42 | .parse_stream(input) 43 | .into_result() 44 | } 45 | 46 | pub fn selection<'a, S>( 47 | input: &mut TokenStream<'a>, 48 | ) -> StdParseResult, TokenStream<'a>> 49 | where 50 | S: Text<'a>, 51 | { 52 | parser(field) 53 | .map(Selection::Field) 54 | .or(punct("...").with( 55 | ( 56 | position(), 57 | optional(ident("on").with(name::<'a, S>()).map(TypeCondition::On)), 58 | parser(directives), 59 | parser(selection_set), 60 | ) 61 | .map( 62 | |(position, type_condition, directives, selection_set)| InlineFragment { 63 | position, 64 | type_condition, 65 | selection_set, 66 | directives, 67 | }, 68 | ) 69 | .map(Selection::InlineFragment) 70 | .or((position(), name::<'a, S>(), parser(directives)) 71 | .map(|(position, fragment_name, directives)| FragmentSpread { 72 | position, 73 | fragment_name, 74 | directives, 75 | }) 76 | .map(Selection::FragmentSpread)), 77 | )) 78 | .parse_stream(input) 79 | .into_result() 80 | } 81 | 82 | pub fn selection_set<'a, S>( 83 | input: &mut TokenStream<'a>, 84 | ) -> StdParseResult, TokenStream<'a>> 85 | where 86 | S: Text<'a>, 87 | { 88 | ( 89 | position().skip(punct("{")), 90 | many1(parser(selection)), 91 | position().skip(punct("}")), 92 | ) 93 | .map(|(start, items, end)| SelectionSet { 94 | span: (start, end), 95 | items, 96 | }) 97 | .parse_stream(input) 98 | .into_result() 99 | } 100 | 101 | pub fn query<'a, T: Text<'a>>( 102 | input: &mut TokenStream<'a>, 103 | ) -> StdParseResult, TokenStream<'a>> { 104 | position() 105 | .skip(ident("query")) 106 | .and(parser(operation_common)) 107 | .map( 108 | |(position, (name, variable_definitions, directives, selection_set))| Query { 109 | position, 110 | name, 111 | selection_set, 112 | variable_definitions, 113 | directives, 114 | }, 115 | ) 116 | .parse_stream(input) 117 | .into_result() 118 | } 119 | 120 | /// A set of attributes common to a Query and a Mutation 121 | #[allow(type_alias_bounds)] 122 | type OperationCommon<'a, T: Text<'a>> = ( 123 | Option, 124 | Vec>, 125 | Vec>, 126 | SelectionSet<'a, T>, 127 | ); 128 | 129 | pub fn operation_common<'a, T: Text<'a>>( 130 | input: &mut TokenStream<'a>, 131 | ) -> StdParseResult, TokenStream<'a>> { 132 | optional(name::<'a, T>()) 133 | .and( 134 | optional( 135 | punct("(") 136 | .with(many1( 137 | ( 138 | position(), 139 | punct("$").with(name::<'a, T>()).skip(punct(":")), 140 | parser(parse_type), 141 | optional(punct("=").with(parser(default_value))), 142 | ) 143 | .map( 144 | |(position, name, var_type, default_value)| VariableDefinition { 145 | position, 146 | name, 147 | var_type, 148 | default_value, 149 | }, 150 | ), 151 | )) 152 | .skip(punct(")")), 153 | ) 154 | .map(|vars| vars.unwrap_or_default()), 155 | ) 156 | .and(parser(directives)) 157 | .and(parser(selection_set)) 158 | .map(|(((a, b), c), d)| (a, b, c, d)) 159 | .parse_stream(input) 160 | .into_result() 161 | } 162 | 163 | pub fn mutation<'a, T: Text<'a>>( 164 | input: &mut TokenStream<'a>, 165 | ) -> StdParseResult, TokenStream<'a>> { 166 | position() 167 | .skip(ident("mutation")) 168 | .and(parser(operation_common)) 169 | .map( 170 | |(position, (name, variable_definitions, directives, selection_set))| Mutation { 171 | position, 172 | name, 173 | selection_set, 174 | variable_definitions, 175 | directives, 176 | }, 177 | ) 178 | .parse_stream(input) 179 | .into_result() 180 | } 181 | 182 | pub fn subscription<'a, T: Text<'a>>( 183 | input: &mut TokenStream<'a>, 184 | ) -> StdParseResult, TokenStream<'a>> { 185 | position() 186 | .skip(ident("subscription")) 187 | .and(parser(operation_common)) 188 | .map( 189 | |(position, (name, variable_definitions, directives, selection_set))| Subscription { 190 | position, 191 | name, 192 | selection_set, 193 | variable_definitions, 194 | directives, 195 | }, 196 | ) 197 | .parse_stream(input) 198 | .into_result() 199 | } 200 | 201 | pub fn operation_definition<'a, S>( 202 | input: &mut TokenStream<'a>, 203 | ) -> StdParseResult, TokenStream<'a>> 204 | where 205 | S: Text<'a>, 206 | { 207 | parser(selection_set) 208 | .map(OperationDefinition::SelectionSet) 209 | .or(parser(query).map(OperationDefinition::Query)) 210 | .or(parser(mutation).map(OperationDefinition::Mutation)) 211 | .or(parser(subscription).map(OperationDefinition::Subscription)) 212 | .parse_stream(input) 213 | .into_result() 214 | } 215 | 216 | pub fn fragment_definition<'a, T: Text<'a>>( 217 | input: &mut TokenStream<'a>, 218 | ) -> StdParseResult, TokenStream<'a>> { 219 | ( 220 | position().skip(ident("fragment")), 221 | name::<'a, T>(), 222 | ident("on").with(name::<'a, T>()).map(TypeCondition::On), 223 | parser(directives), 224 | parser(selection_set), 225 | ) 226 | .map( 227 | |(position, name, type_condition, directives, selection_set)| FragmentDefinition { 228 | position, 229 | name, 230 | type_condition, 231 | directives, 232 | selection_set, 233 | }, 234 | ) 235 | .parse_stream(input) 236 | .into_result() 237 | } 238 | 239 | pub fn definition<'a, S>( 240 | input: &mut TokenStream<'a>, 241 | ) -> StdParseResult, TokenStream<'a>> 242 | where 243 | S: Text<'a>, 244 | { 245 | parser(operation_definition) 246 | .map(Definition::Operation) 247 | .or(parser(fragment_definition).map(Definition::Fragment)) 248 | .parse_stream(input) 249 | .into_result() 250 | } 251 | 252 | /// Parses a piece of query language and returns an AST 253 | pub fn parse_query<'a, S>(s: &'a str) -> Result, ParseError> 254 | where 255 | S: Text<'a>, 256 | { 257 | let mut tokens = TokenStream::new(s); 258 | let (doc, _) = many1(parser(definition)) 259 | .map(|d| Document { definitions: d }) 260 | .skip(eof()) 261 | .parse_stream(&mut tokens) 262 | .into_result() 263 | .map_err(|e| e.into_inner().error)?; 264 | 265 | Ok(doc) 266 | } 267 | 268 | /// Parses a single ExecutableDefinition and returns an AST as well as the 269 | /// remainder of the input which is unparsed 270 | pub fn consume_definition<'a, S>(s: &'a str) -> Result<(Definition<'a, S>, &'a str), ParseError> 271 | where 272 | S: Text<'a>, 273 | { 274 | let tokens = TokenStream::new(s); 275 | let (doc, tokens) = parser(definition).parse(tokens)?; 276 | 277 | Ok((doc, &s[tokens.offset()..])) 278 | } 279 | 280 | #[cfg(test)] 281 | mod test { 282 | use super::{consume_definition, parse_query}; 283 | use crate::position::Pos; 284 | use crate::query::grammar::*; 285 | 286 | fn ast(s: &str) -> Document { 287 | parse_query::(s).unwrap().to_owned() 288 | } 289 | 290 | #[test] 291 | fn one_field() { 292 | assert_eq!( 293 | ast("{ a }"), 294 | Document { 295 | definitions: vec![Definition::Operation(OperationDefinition::SelectionSet( 296 | SelectionSet { 297 | span: (Pos { line: 1, column: 1 }, Pos { line: 1, column: 5 }), 298 | items: vec![Selection::Field(Field { 299 | position: Pos { line: 1, column: 3 }, 300 | alias: None, 301 | name: "a".into(), 302 | arguments: Vec::new(), 303 | directives: Vec::new(), 304 | selection_set: SelectionSet { 305 | span: (Pos { line: 1, column: 3 }, Pos { line: 1, column: 3 }), 306 | items: Vec::new() 307 | }, 308 | }),], 309 | } 310 | ))], 311 | } 312 | ); 313 | } 314 | 315 | #[test] 316 | fn builtin_values() { 317 | assert_eq!( 318 | ast("{ a(t: true, f: false, n: null) }"), 319 | Document { 320 | definitions: vec![Definition::Operation(OperationDefinition::SelectionSet( 321 | SelectionSet { 322 | span: ( 323 | Pos { line: 1, column: 1 }, 324 | Pos { 325 | line: 1, 326 | column: 33 327 | } 328 | ), 329 | items: vec![Selection::Field(Field { 330 | position: Pos { line: 1, column: 3 }, 331 | alias: None, 332 | name: "a".into(), 333 | arguments: vec![ 334 | ("t".into(), Value::Boolean(true)), 335 | ("f".into(), Value::Boolean(false)), 336 | ("n".into(), Value::Null), 337 | ], 338 | directives: Vec::new(), 339 | selection_set: SelectionSet { 340 | span: (Pos { line: 1, column: 3 }, Pos { line: 1, column: 3 }), 341 | items: Vec::new() 342 | }, 343 | }),], 344 | } 345 | ))], 346 | } 347 | ); 348 | } 349 | 350 | #[test] 351 | fn one_field_roundtrip() { 352 | assert_eq!(ast("{ a }").to_string(), "{\n a\n}\n"); 353 | } 354 | 355 | #[test] 356 | #[should_panic(expected = "number too large")] 357 | fn large_integer() { 358 | ast("{ a(x: 10000000000000000000000000000 }"); 359 | } 360 | 361 | #[test] 362 | fn consume_single_query() { 363 | let (query, remainder) = consume_definition::("query { a } query { b }").unwrap(); 364 | assert!(matches!(query, Definition::Operation(_))); 365 | assert_eq!(remainder, "query { b }"); 366 | } 367 | 368 | #[test] 369 | fn consume_full_text() { 370 | let (query, remainder) = consume_definition::("query { a }").unwrap(); 371 | assert!(matches!(query, Definition::Operation(_))); 372 | assert_eq!(remainder, ""); 373 | } 374 | 375 | #[test] 376 | fn consume_single_query_preceding_non_graphql() { 377 | let (query, remainder) = 378 | consume_definition::("query { a } where a > 1 => 10.0").unwrap(); 379 | assert!(matches!(query, Definition::Operation(_))); 380 | assert_eq!(remainder, "where a > 1 => 10.0"); 381 | } 382 | 383 | #[test] 384 | fn consume_fails_without_operation() { 385 | let err = consume_definition::("where a > 1 => 10.0") 386 | .expect_err("Expected parse to fail with an error"); 387 | let err = format!("{}", err); 388 | assert_eq!(err, "query parse error: Parse error at 1:1\nUnexpected `where[Name]`\nExpected {, query, mutation, subscription or fragment\n"); 389 | } 390 | 391 | #[test] 392 | fn recursion_too_deep() { 393 | let query = format!( 394 | "{}(b: {}{}){}", 395 | "{ a".repeat(30), 396 | "[".repeat(25), 397 | "]".repeat(25), 398 | "}".repeat(30) 399 | ); 400 | let result = parse_query::<&str>(&query); 401 | let err = format!("{}", result.unwrap_err()); 402 | assert_eq!( 403 | &err, 404 | "query parse error: Parse error at 1:114\nExpected ]\nRecursion limit exceeded\n" 405 | ) 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/query/minify.rs: -------------------------------------------------------------------------------- 1 | use crate::tokenizer::{Kind, Token, TokenStream}; 2 | use combine::StreamOnce; 3 | use thiserror::Error; 4 | 5 | /// Error minifying query 6 | #[derive(Error, Debug)] 7 | #[error("query minify error: {}", _0)] 8 | pub struct MinifyError(String); 9 | 10 | pub fn minify_query(source: String) -> Result { 11 | let mut bits: Vec<&str> = Vec::new(); 12 | let mut stream = TokenStream::new(source.as_str()); 13 | let mut prev_was_punctuator = false; 14 | 15 | loop { 16 | match stream.uncons() { 17 | Ok(x) => { 18 | let token: Token = x; 19 | let is_non_punctuator = token.kind != Kind::Punctuator; 20 | 21 | if prev_was_punctuator && is_non_punctuator { 22 | bits.push(" "); 23 | } 24 | 25 | bits.push(token.value); 26 | prev_was_punctuator = is_non_punctuator; 27 | } 28 | Err(ref e) if e == &combine::easy::Error::end_of_input() => break, 29 | Err(e) => return Err(MinifyError(e.to_string())), 30 | } 31 | } 32 | 33 | Ok(bits.join("")) 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | #[test] 39 | fn strip_ignored_characters() { 40 | let source = " 41 | query SomeQuery($foo: String!, $bar: String) { 42 | someField(foo: $foo, bar: $bar) { 43 | a 44 | b { 45 | ... on B { 46 | c 47 | d 48 | } 49 | } 50 | } 51 | } 52 | "; 53 | 54 | let minified = super::minify_query(source.to_string()).expect("minification failed"); 55 | 56 | assert_eq!( 57 | &minified, 58 | "query SomeQuery($foo:String!$bar:String){someField(foo:$foo bar:$bar){a b{...on B{c d}}}}" 59 | ); 60 | } 61 | 62 | #[test] 63 | fn unexpected_token() { 64 | let source = " 65 | query foo { 66 | bar; 67 | } 68 | "; 69 | 70 | let minified = super::minify_query(source.to_string()); 71 | 72 | assert!(minified.is_err()); 73 | 74 | assert_eq!( 75 | minified.unwrap_err().to_string(), 76 | "query minify error: Unexpected unexpected character ';'" 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/query/mod.rs: -------------------------------------------------------------------------------- 1 | //! Query language AST and parsing utilities 2 | //! 3 | mod ast; 4 | mod error; 5 | mod format; 6 | mod grammar; 7 | mod minify; 8 | 9 | pub use self::ast::*; 10 | pub use self::error::ParseError; 11 | pub use self::grammar::{consume_definition, parse_query}; 12 | pub use self::minify::minify_query; 13 | -------------------------------------------------------------------------------- /src/schema/ast.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use thiserror::Error; 4 | 5 | pub use crate::common::{Directive, Text, Type, Value}; 6 | use crate::position::Pos; 7 | 8 | #[derive(Debug, Clone, Default, PartialEq)] 9 | pub struct Document<'a, T: Text<'a>> { 10 | pub definitions: Vec>, 11 | } 12 | 13 | impl<'a> Document<'a, String> { 14 | pub fn into_static(self) -> Document<'static, String> { 15 | // To support both reference and owned values in the AST, 16 | // all string data is represented with the ::common::Str<'a, T: Text<'a>> 17 | // wrapper type. 18 | // This type must carry the liftetime of the schema string, 19 | // and is stored in a PhantomData value on the Str type. 20 | // When using owned String types, the actual lifetime of 21 | // the Ast nodes is 'static, since no references are kept, 22 | // but the nodes will still carry the input lifetime. 23 | // To continue working with Document in a owned fasion 24 | // the lifetime needs to be transmuted to 'static. 25 | // 26 | // This is safe because no references are present. 27 | // Just the PhantomData lifetime reference is transmuted away. 28 | unsafe { std::mem::transmute::<_, Document<'static, String>>(self) } 29 | } 30 | } 31 | 32 | #[derive(Debug, Clone, PartialEq)] 33 | pub enum Definition<'a, T: Text<'a>> { 34 | SchemaDefinition(SchemaDefinition<'a, T>), 35 | TypeDefinition(TypeDefinition<'a, T>), 36 | TypeExtension(TypeExtension<'a, T>), 37 | DirectiveDefinition(DirectiveDefinition<'a, T>), 38 | } 39 | 40 | #[derive(Debug, Clone, Default, PartialEq)] 41 | pub struct SchemaDefinition<'a, T: Text<'a>> { 42 | pub position: Pos, 43 | pub directives: Vec>, 44 | pub query: Option, 45 | pub mutation: Option, 46 | pub subscription: Option, 47 | } 48 | 49 | #[derive(Debug, Clone, PartialEq)] 50 | pub enum TypeDefinition<'a, T: Text<'a>> { 51 | Scalar(ScalarType<'a, T>), 52 | Object(ObjectType<'a, T>), 53 | Interface(InterfaceType<'a, T>), 54 | Union(UnionType<'a, T>), 55 | Enum(EnumType<'a, T>), 56 | InputObject(InputObjectType<'a, T>), 57 | } 58 | 59 | #[derive(Debug, Clone, PartialEq)] 60 | pub enum TypeExtension<'a, T: Text<'a>> { 61 | Scalar(ScalarTypeExtension<'a, T>), 62 | Object(ObjectTypeExtension<'a, T>), 63 | Interface(InterfaceTypeExtension<'a, T>), 64 | Union(UnionTypeExtension<'a, T>), 65 | Enum(EnumTypeExtension<'a, T>), 66 | InputObject(InputObjectTypeExtension<'a, T>), 67 | } 68 | 69 | #[derive(Debug, Clone, PartialEq)] 70 | pub struct ScalarType<'a, T: Text<'a>> { 71 | pub position: Pos, 72 | pub description: Option, 73 | pub name: T::Value, 74 | pub directives: Vec>, 75 | } 76 | 77 | impl<'a, T> ScalarType<'a, T> 78 | where 79 | T: Text<'a>, 80 | { 81 | pub fn new(name: T::Value) -> Self { 82 | Self { 83 | position: Pos::default(), 84 | description: None, 85 | name, 86 | directives: vec![], 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, Clone, PartialEq)] 92 | pub struct ScalarTypeExtension<'a, T: Text<'a>> { 93 | pub position: Pos, 94 | pub name: T::Value, 95 | pub directives: Vec>, 96 | } 97 | 98 | impl<'a, T> ScalarTypeExtension<'a, T> 99 | where 100 | T: Text<'a>, 101 | { 102 | pub fn new(name: T::Value) -> Self { 103 | Self { 104 | position: Pos::default(), 105 | name, 106 | directives: vec![], 107 | } 108 | } 109 | } 110 | 111 | #[derive(Debug, Clone, PartialEq)] 112 | pub struct ObjectType<'a, T: Text<'a>> { 113 | pub position: Pos, 114 | pub description: Option, 115 | pub name: T::Value, 116 | pub implements_interfaces: Vec, 117 | pub directives: Vec>, 118 | pub fields: Vec>, 119 | } 120 | 121 | impl<'a, T> ObjectType<'a, T> 122 | where 123 | T: Text<'a>, 124 | { 125 | pub fn new(name: T::Value) -> Self { 126 | Self { 127 | position: Pos::default(), 128 | description: None, 129 | name, 130 | implements_interfaces: vec![], 131 | directives: vec![], 132 | fields: vec![], 133 | } 134 | } 135 | } 136 | 137 | #[derive(Debug, Clone, PartialEq)] 138 | pub struct ObjectTypeExtension<'a, T: Text<'a>> { 139 | pub position: Pos, 140 | pub name: T::Value, 141 | pub implements_interfaces: Vec, 142 | pub directives: Vec>, 143 | pub fields: Vec>, 144 | } 145 | 146 | impl<'a, T> ObjectTypeExtension<'a, T> 147 | where 148 | T: Text<'a>, 149 | { 150 | pub fn new(name: T::Value) -> Self { 151 | Self { 152 | position: Pos::default(), 153 | name, 154 | implements_interfaces: vec![], 155 | directives: vec![], 156 | fields: vec![], 157 | } 158 | } 159 | } 160 | 161 | #[derive(Debug, Clone, PartialEq)] 162 | pub struct Field<'a, T: Text<'a>> { 163 | pub position: Pos, 164 | pub description: Option, 165 | pub name: T::Value, 166 | pub arguments: Vec>, 167 | pub field_type: Type<'a, T>, 168 | pub directives: Vec>, 169 | } 170 | 171 | #[derive(Debug, Clone, PartialEq)] 172 | pub struct InputValue<'a, T: Text<'a>> { 173 | pub position: Pos, 174 | pub description: Option, 175 | pub name: T::Value, 176 | pub value_type: Type<'a, T>, 177 | pub default_value: Option>, 178 | pub directives: Vec>, 179 | } 180 | 181 | #[derive(Debug, Clone, PartialEq)] 182 | pub struct InterfaceType<'a, T: Text<'a>> { 183 | pub position: Pos, 184 | pub description: Option, 185 | pub name: T::Value, 186 | pub implements_interfaces: Vec, 187 | pub directives: Vec>, 188 | pub fields: Vec>, 189 | } 190 | 191 | impl<'a, T> InterfaceType<'a, T> 192 | where 193 | T: Text<'a>, 194 | { 195 | pub fn new(name: T::Value) -> Self { 196 | Self { 197 | position: Pos::default(), 198 | description: None, 199 | name, 200 | implements_interfaces: vec![], 201 | directives: vec![], 202 | fields: vec![], 203 | } 204 | } 205 | } 206 | 207 | #[derive(Debug, Clone, PartialEq)] 208 | pub struct InterfaceTypeExtension<'a, T: Text<'a>> { 209 | pub position: Pos, 210 | pub name: T::Value, 211 | pub implements_interfaces: Vec, 212 | pub directives: Vec>, 213 | pub fields: Vec>, 214 | } 215 | 216 | impl<'a, T> InterfaceTypeExtension<'a, T> 217 | where 218 | T: Text<'a>, 219 | { 220 | pub fn new(name: T::Value) -> Self { 221 | Self { 222 | position: Pos::default(), 223 | name, 224 | implements_interfaces: vec![], 225 | directives: vec![], 226 | fields: vec![], 227 | } 228 | } 229 | } 230 | 231 | #[derive(Debug, Clone, PartialEq)] 232 | pub struct UnionType<'a, T: Text<'a>> { 233 | pub position: Pos, 234 | pub description: Option, 235 | pub name: T::Value, 236 | pub directives: Vec>, 237 | pub types: Vec, 238 | } 239 | 240 | impl<'a, T> UnionType<'a, T> 241 | where 242 | T: Text<'a>, 243 | { 244 | pub fn new(name: T::Value) -> Self { 245 | Self { 246 | position: Pos::default(), 247 | description: None, 248 | name, 249 | directives: vec![], 250 | types: vec![], 251 | } 252 | } 253 | } 254 | 255 | #[derive(Debug, Clone, PartialEq)] 256 | pub struct UnionTypeExtension<'a, T: Text<'a>> { 257 | pub position: Pos, 258 | pub name: T::Value, 259 | pub directives: Vec>, 260 | pub types: Vec, 261 | } 262 | 263 | impl<'a, T> UnionTypeExtension<'a, T> 264 | where 265 | T: Text<'a>, 266 | { 267 | pub fn new(name: T::Value) -> Self { 268 | Self { 269 | position: Pos::default(), 270 | name, 271 | directives: vec![], 272 | types: vec![], 273 | } 274 | } 275 | } 276 | 277 | #[derive(Debug, Clone, PartialEq)] 278 | pub struct EnumType<'a, T: Text<'a>> { 279 | pub position: Pos, 280 | pub description: Option, 281 | pub name: T::Value, 282 | pub directives: Vec>, 283 | pub values: Vec>, 284 | } 285 | 286 | impl<'a, T> EnumType<'a, T> 287 | where 288 | T: Text<'a>, 289 | { 290 | pub fn new(name: T::Value) -> Self { 291 | Self { 292 | position: Pos::default(), 293 | description: None, 294 | name, 295 | directives: vec![], 296 | values: vec![], 297 | } 298 | } 299 | } 300 | 301 | #[derive(Debug, Clone, PartialEq)] 302 | pub struct EnumValue<'a, T: Text<'a>> { 303 | pub position: Pos, 304 | pub description: Option, 305 | pub name: T::Value, 306 | pub directives: Vec>, 307 | } 308 | 309 | impl<'a, T> EnumValue<'a, T> 310 | where 311 | T: Text<'a>, 312 | { 313 | pub fn new(name: T::Value) -> Self { 314 | Self { 315 | position: Pos::default(), 316 | description: None, 317 | name, 318 | directives: vec![], 319 | } 320 | } 321 | } 322 | 323 | #[derive(Debug, Clone, PartialEq)] 324 | pub struct EnumTypeExtension<'a, T: Text<'a>> { 325 | pub position: Pos, 326 | pub name: T::Value, 327 | pub directives: Vec>, 328 | pub values: Vec>, 329 | } 330 | 331 | impl<'a, T> EnumTypeExtension<'a, T> 332 | where 333 | T: Text<'a>, 334 | { 335 | pub fn new(name: T::Value) -> Self { 336 | Self { 337 | position: Pos::default(), 338 | name, 339 | directives: vec![], 340 | values: vec![], 341 | } 342 | } 343 | } 344 | 345 | #[derive(Debug, Clone, PartialEq)] 346 | pub struct InputObjectType<'a, T: Text<'a>> { 347 | pub position: Pos, 348 | pub description: Option, 349 | pub name: T::Value, 350 | pub directives: Vec>, 351 | pub fields: Vec>, 352 | } 353 | 354 | impl<'a, T> InputObjectType<'a, T> 355 | where 356 | T: Text<'a>, 357 | { 358 | pub fn new(name: T::Value) -> Self { 359 | Self { 360 | position: Pos::default(), 361 | description: None, 362 | name, 363 | directives: vec![], 364 | fields: vec![], 365 | } 366 | } 367 | } 368 | 369 | #[derive(Debug, Clone, PartialEq)] 370 | pub struct InputObjectTypeExtension<'a, T: Text<'a>> { 371 | pub position: Pos, 372 | pub name: T::Value, 373 | pub directives: Vec>, 374 | pub fields: Vec>, 375 | } 376 | 377 | impl<'a, T> InputObjectTypeExtension<'a, T> 378 | where 379 | T: Text<'a>, 380 | { 381 | pub fn new(name: T::Value) -> Self { 382 | Self { 383 | position: Pos::default(), 384 | name, 385 | directives: vec![], 386 | fields: vec![], 387 | } 388 | } 389 | } 390 | 391 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 392 | pub enum DirectiveLocation { 393 | // executable 394 | Query, 395 | Mutation, 396 | Subscription, 397 | Field, 398 | FragmentDefinition, 399 | FragmentSpread, 400 | InlineFragment, 401 | 402 | // type_system 403 | Schema, 404 | Scalar, 405 | Object, 406 | FieldDefinition, 407 | ArgumentDefinition, 408 | Interface, 409 | Union, 410 | Enum, 411 | EnumValue, 412 | InputObject, 413 | InputFieldDefinition, 414 | VariableDefinition, 415 | } 416 | 417 | #[derive(Debug, Clone, PartialEq)] 418 | pub struct DirectiveDefinition<'a, T: Text<'a>> { 419 | pub position: Pos, 420 | pub description: Option, 421 | pub name: T::Value, 422 | pub arguments: Vec>, 423 | pub repeatable: bool, 424 | pub locations: Vec, 425 | } 426 | 427 | impl<'a, T> DirectiveDefinition<'a, T> 428 | where 429 | T: Text<'a>, 430 | { 431 | pub fn new(name: T::Value) -> Self { 432 | Self { 433 | position: Pos::default(), 434 | description: None, 435 | name, 436 | arguments: vec![], 437 | repeatable: false, 438 | locations: vec![], 439 | } 440 | } 441 | } 442 | 443 | impl DirectiveLocation { 444 | /// Returns GraphQL syntax compatible name of the directive 445 | pub fn as_str(&self) -> &'static str { 446 | use self::DirectiveLocation::*; 447 | match *self { 448 | Query => "QUERY", 449 | Mutation => "MUTATION", 450 | Subscription => "SUBSCRIPTION", 451 | Field => "FIELD", 452 | FragmentDefinition => "FRAGMENT_DEFINITION", 453 | FragmentSpread => "FRAGMENT_SPREAD", 454 | InlineFragment => "INLINE_FRAGMENT", 455 | Schema => "SCHEMA", 456 | Scalar => "SCALAR", 457 | Object => "OBJECT", 458 | FieldDefinition => "FIELD_DEFINITION", 459 | ArgumentDefinition => "ARGUMENT_DEFINITION", 460 | Interface => "INTERFACE", 461 | Union => "UNION", 462 | Enum => "ENUM", 463 | EnumValue => "ENUM_VALUE", 464 | InputObject => "INPUT_OBJECT", 465 | InputFieldDefinition => "INPUT_FIELD_DEFINITION", 466 | VariableDefinition => "VARIABLE_DEFINITION", 467 | } 468 | } 469 | 470 | /// Returns `true` if this location is for queries (execution) 471 | pub fn is_query(&self) -> bool { 472 | use self::DirectiveLocation::*; 473 | match *self { 474 | Query | Mutation | Subscription | Field | FragmentDefinition | FragmentSpread 475 | | InlineFragment => true, 476 | 477 | Schema | Scalar | Object | FieldDefinition | ArgumentDefinition | Interface | Union 478 | | Enum | EnumValue | InputObject | InputFieldDefinition | VariableDefinition => false, 479 | } 480 | } 481 | 482 | /// Returns `true` if this location is for schema 483 | pub fn is_schema(&self) -> bool { 484 | !self.is_query() 485 | } 486 | } 487 | 488 | #[derive(Debug, Error)] 489 | #[error("invalid directive location")] 490 | pub struct InvalidDirectiveLocation; 491 | 492 | impl FromStr for DirectiveLocation { 493 | type Err = InvalidDirectiveLocation; 494 | fn from_str(s: &str) -> Result { 495 | use self::DirectiveLocation::*; 496 | let val = match s { 497 | "QUERY" => Query, 498 | "MUTATION" => Mutation, 499 | "SUBSCRIPTION" => Subscription, 500 | "FIELD" => Field, 501 | "FRAGMENT_DEFINITION" => FragmentDefinition, 502 | "FRAGMENT_SPREAD" => FragmentSpread, 503 | "INLINE_FRAGMENT" => InlineFragment, 504 | "SCHEMA" => Schema, 505 | "SCALAR" => Scalar, 506 | "OBJECT" => Object, 507 | "FIELD_DEFINITION" => FieldDefinition, 508 | "ARGUMENT_DEFINITION" => ArgumentDefinition, 509 | "INTERFACE" => Interface, 510 | "UNION" => Union, 511 | "ENUM" => Enum, 512 | "ENUM_VALUE" => EnumValue, 513 | "INPUT_OBJECT" => InputObject, 514 | "INPUT_FIELD_DEFINITION" => InputFieldDefinition, 515 | "VARIABLE_DEFINITION" => VariableDefinition, 516 | _ => return Err(InvalidDirectiveLocation), 517 | }; 518 | 519 | Ok(val) 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /src/schema/error.rs: -------------------------------------------------------------------------------- 1 | use combine::easy::Errors; 2 | use thiserror::Error; 3 | 4 | use crate::position::Pos; 5 | use crate::tokenizer::Token; 6 | 7 | pub type InternalError<'a> = Errors, Token<'a>, Pos>; 8 | 9 | /// Error parsing schema 10 | /// 11 | /// This structure is opaque for forward compatibility. We are exploring a 12 | /// way to improve both error message and API. 13 | #[derive(Error, Debug)] 14 | #[error("schema parse error: {}", _0)] 15 | pub struct ParseError(String); 16 | 17 | impl<'a> From> for ParseError { 18 | fn from(e: InternalError<'a>) -> ParseError { 19 | ParseError(format!("{}", e)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/format.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::common::Text; 4 | use crate::format::{format_directives, Displayable, Formatter, Style}; 5 | 6 | use crate::schema::ast::*; 7 | 8 | impl<'a, T> Document<'a, T> 9 | where 10 | T: Text<'a>, 11 | { 12 | /// Format a document according to style 13 | pub fn format(&self, style: &Style) -> String { 14 | let mut formatter = Formatter::new(style); 15 | self.display(&mut formatter); 16 | formatter.into_string() 17 | } 18 | } 19 | 20 | fn to_string(v: &T) -> String { 21 | let style = Style::default(); 22 | let mut formatter = Formatter::new(&style); 23 | v.display(&mut formatter); 24 | formatter.into_string() 25 | } 26 | 27 | fn description(description: &Option, f: &mut Formatter) { 28 | if let Some(ref descr) = *description { 29 | f.indent(); 30 | f.write_quoted(descr.as_ref()); 31 | f.endline(); 32 | } 33 | } 34 | 35 | impl<'a, T> Displayable for Document<'a, T> 36 | where 37 | T: Text<'a>, 38 | { 39 | fn display(&self, f: &mut Formatter) { 40 | for item in &self.definitions { 41 | item.display(f); 42 | } 43 | } 44 | } 45 | 46 | impl<'a, T> Displayable for Definition<'a, T> 47 | where 48 | T: Text<'a>, 49 | { 50 | fn display(&self, f: &mut Formatter) { 51 | f.margin(); 52 | match *self { 53 | Definition::SchemaDefinition(ref s) => s.display(f), 54 | Definition::TypeDefinition(ref t) => t.display(f), 55 | Definition::TypeExtension(ref e) => e.display(f), 56 | Definition::DirectiveDefinition(ref d) => d.display(f), 57 | } 58 | } 59 | } 60 | 61 | impl<'a, T> Displayable for SchemaDefinition<'a, T> 62 | where 63 | T: Text<'a>, 64 | { 65 | fn display(&self, f: &mut Formatter) { 66 | f.indent(); 67 | f.write("schema"); 68 | format_directives(&self.directives, f); 69 | f.write(" "); 70 | f.start_block(); 71 | if let Some(ref q) = self.query { 72 | f.indent(); 73 | f.write("query: "); 74 | f.write(q.as_ref()); 75 | f.endline(); 76 | } 77 | if let Some(ref m) = self.mutation { 78 | f.indent(); 79 | f.write("mutation: "); 80 | f.write(m.as_ref()); 81 | f.endline(); 82 | } 83 | if let Some(ref s) = self.subscription { 84 | f.indent(); 85 | f.write("subscription: "); 86 | f.write(s.as_ref()); 87 | f.endline(); 88 | } 89 | f.end_block(); 90 | } 91 | } 92 | 93 | impl<'a, T> Displayable for TypeDefinition<'a, T> 94 | where 95 | T: Text<'a>, 96 | { 97 | fn display(&self, f: &mut Formatter) { 98 | match *self { 99 | TypeDefinition::Scalar(ref s) => s.display(f), 100 | TypeDefinition::Object(ref o) => o.display(f), 101 | TypeDefinition::Interface(ref i) => i.display(f), 102 | TypeDefinition::Union(ref u) => u.display(f), 103 | TypeDefinition::Enum(ref e) => e.display(f), 104 | TypeDefinition::InputObject(ref i) => i.display(f), 105 | } 106 | } 107 | } 108 | 109 | impl<'a, T> Displayable for ScalarType<'a, T> 110 | where 111 | T: Text<'a>, 112 | { 113 | fn display(&self, f: &mut Formatter) { 114 | description(&self.description, f); 115 | f.indent(); 116 | f.write("scalar "); 117 | f.write(self.name.as_ref()); 118 | format_directives(&self.directives, f); 119 | f.endline(); 120 | } 121 | } 122 | 123 | impl<'a, T> Displayable for ScalarTypeExtension<'a, T> 124 | where 125 | T: Text<'a>, 126 | { 127 | fn display(&self, f: &mut Formatter) { 128 | f.indent(); 129 | f.write("extend scalar "); 130 | f.write(self.name.as_ref()); 131 | format_directives(&self.directives, f); 132 | f.endline(); 133 | } 134 | } 135 | 136 | fn format_fields<'a, T>(fields: &[Field<'a, T>], f: &mut Formatter) 137 | where 138 | T: Text<'a>, 139 | { 140 | if !fields.is_empty() { 141 | f.write(" "); 142 | f.start_block(); 143 | for fld in fields { 144 | fld.display(f); 145 | } 146 | f.end_block(); 147 | } else { 148 | f.endline(); 149 | } 150 | } 151 | 152 | impl<'a, T> Displayable for ObjectType<'a, T> 153 | where 154 | T: Text<'a>, 155 | { 156 | fn display(&self, f: &mut Formatter) { 157 | description(&self.description, f); 158 | f.indent(); 159 | f.write("type "); 160 | f.write(self.name.as_ref()); 161 | if !self.implements_interfaces.is_empty() { 162 | f.write(" implements "); 163 | f.write(self.implements_interfaces[0].as_ref()); 164 | for name in &self.implements_interfaces[1..] { 165 | f.write(" & "); 166 | f.write(name.as_ref()); 167 | } 168 | } 169 | format_directives(&self.directives, f); 170 | format_fields(&self.fields, f); 171 | } 172 | } 173 | 174 | impl<'a, T> Displayable for ObjectTypeExtension<'a, T> 175 | where 176 | T: Text<'a>, 177 | { 178 | fn display(&self, f: &mut Formatter) { 179 | f.indent(); 180 | f.write("extend type "); 181 | f.write(self.name.as_ref()); 182 | if !self.implements_interfaces.is_empty() { 183 | f.write(" implements "); 184 | f.write(self.implements_interfaces[0].as_ref()); 185 | for name in &self.implements_interfaces[1..] { 186 | f.write(" & "); 187 | f.write(name.as_ref()); 188 | } 189 | } 190 | format_directives(&self.directives, f); 191 | format_fields(&self.fields, f); 192 | } 193 | } 194 | 195 | impl<'a, T> Displayable for InputValue<'a, T> 196 | where 197 | T: Text<'a>, 198 | { 199 | fn display(&self, f: &mut Formatter) { 200 | if let Some(ref descr) = self.description { 201 | f.write_quoted(descr.as_ref()); 202 | f.write(" "); 203 | } 204 | f.write(self.name.as_ref()); 205 | f.write(": "); 206 | self.value_type.display(f); 207 | if let Some(ref def) = self.default_value { 208 | f.write(" = "); 209 | def.display(f); 210 | } 211 | format_directives(&self.directives, f); 212 | } 213 | } 214 | 215 | fn format_arguments<'a, T>(arguments: &[InputValue<'a, T>], f: &mut Formatter) 216 | where 217 | T: Text<'a>, 218 | { 219 | if !arguments.is_empty() { 220 | f.write("("); 221 | arguments[0].display(f); 222 | for arg in &arguments[1..] { 223 | f.write(", "); 224 | arg.display(f); 225 | } 226 | f.write(")"); 227 | } 228 | } 229 | 230 | impl<'a, T> Displayable for Field<'a, T> 231 | where 232 | T: Text<'a>, 233 | { 234 | fn display(&self, f: &mut Formatter) { 235 | description(&self.description, f); 236 | f.indent(); 237 | f.write(self.name.as_ref()); 238 | format_arguments(&self.arguments, f); 239 | f.write(": "); 240 | self.field_type.display(f); 241 | format_directives(&self.directives, f); 242 | f.endline(); 243 | } 244 | } 245 | 246 | impl<'a, T> Displayable for InterfaceType<'a, T> 247 | where 248 | T: Text<'a>, 249 | { 250 | fn display(&self, f: &mut Formatter) { 251 | description(&self.description, f); 252 | f.indent(); 253 | f.write("interface "); 254 | f.write(self.name.as_ref()); 255 | if !self.implements_interfaces.is_empty() { 256 | f.write(" implements "); 257 | f.write(self.implements_interfaces[0].as_ref()); 258 | for name in &self.implements_interfaces[1..] { 259 | f.write(" & "); 260 | f.write(name.as_ref()); 261 | } 262 | } 263 | format_directives(&self.directives, f); 264 | format_fields(&self.fields, f); 265 | } 266 | } 267 | 268 | impl<'a, T> Displayable for InterfaceTypeExtension<'a, T> 269 | where 270 | T: Text<'a>, 271 | { 272 | fn display(&self, f: &mut Formatter) { 273 | f.indent(); 274 | f.write("extend interface "); 275 | f.write(self.name.as_ref()); 276 | if !self.implements_interfaces.is_empty() { 277 | f.write(" implements "); 278 | f.write(self.implements_interfaces[0].as_ref()); 279 | for name in &self.implements_interfaces[1..] { 280 | f.write(" & "); 281 | f.write(name.as_ref()); 282 | } 283 | } 284 | format_directives(&self.directives, f); 285 | format_fields(&self.fields, f); 286 | } 287 | } 288 | 289 | impl<'a, T> Displayable for UnionType<'a, T> 290 | where 291 | T: Text<'a>, 292 | { 293 | fn display(&self, f: &mut Formatter) { 294 | description(&self.description, f); 295 | f.indent(); 296 | f.write("union "); 297 | f.write(self.name.as_ref()); 298 | format_directives(&self.directives, f); 299 | if !self.types.is_empty() { 300 | f.write(" = "); 301 | f.write(self.types[0].as_ref()); 302 | for typ in &self.types[1..] { 303 | f.write(" | "); 304 | f.write(typ.as_ref()); 305 | } 306 | } 307 | f.endline(); 308 | } 309 | } 310 | 311 | impl<'a, T> Displayable for UnionTypeExtension<'a, T> 312 | where 313 | T: Text<'a>, 314 | { 315 | fn display(&self, f: &mut Formatter) { 316 | f.indent(); 317 | f.write("extend union "); 318 | f.write(self.name.as_ref()); 319 | format_directives(&self.directives, f); 320 | if !self.types.is_empty() { 321 | f.write(" = "); 322 | f.write(self.types[0].as_ref()); 323 | for typ in &self.types[1..] { 324 | f.write(" | "); 325 | f.write(typ.as_ref()); 326 | } 327 | } 328 | f.endline(); 329 | } 330 | } 331 | 332 | impl<'a, T> Displayable for EnumType<'a, T> 333 | where 334 | T: Text<'a>, 335 | { 336 | fn display(&self, f: &mut Formatter) { 337 | description(&self.description, f); 338 | f.indent(); 339 | f.write("enum "); 340 | f.write(self.name.as_ref()); 341 | format_directives(&self.directives, f); 342 | if !self.values.is_empty() { 343 | f.write(" "); 344 | f.start_block(); 345 | for val in &self.values { 346 | f.indent(); 347 | if let Some(ref descr) = val.description { 348 | f.write_quoted(descr.as_ref()); 349 | f.write(" "); 350 | } 351 | f.write(val.name.as_ref()); 352 | format_directives(&val.directives, f); 353 | f.endline(); 354 | } 355 | f.end_block(); 356 | } else { 357 | f.endline(); 358 | } 359 | } 360 | } 361 | 362 | impl<'a, T> Displayable for EnumTypeExtension<'a, T> 363 | where 364 | T: Text<'a>, 365 | { 366 | fn display(&self, f: &mut Formatter) { 367 | f.indent(); 368 | f.write("extend enum "); 369 | f.write(self.name.as_ref()); 370 | format_directives(&self.directives, f); 371 | if !self.values.is_empty() { 372 | f.write(" "); 373 | f.start_block(); 374 | for val in &self.values { 375 | f.indent(); 376 | if let Some(ref descr) = val.description { 377 | f.write_quoted(descr.as_ref()); 378 | f.write(" "); 379 | } 380 | f.write(val.name.as_ref()); 381 | format_directives(&val.directives, f); 382 | f.endline(); 383 | } 384 | f.end_block(); 385 | } else { 386 | f.endline(); 387 | } 388 | } 389 | } 390 | 391 | fn format_inputs<'a, T>(fields: &[InputValue<'a, T>], f: &mut Formatter) 392 | where 393 | T: Text<'a>, 394 | { 395 | if !fields.is_empty() { 396 | f.write(" "); 397 | f.start_block(); 398 | for fld in fields { 399 | f.indent(); 400 | fld.display(f); 401 | f.endline(); 402 | } 403 | f.end_block(); 404 | } else { 405 | f.endline(); 406 | } 407 | } 408 | 409 | impl<'a, T> Displayable for InputObjectType<'a, T> 410 | where 411 | T: Text<'a>, 412 | { 413 | fn display(&self, f: &mut Formatter) { 414 | description(&self.description, f); 415 | f.indent(); 416 | f.write("input "); 417 | f.write(self.name.as_ref()); 418 | format_directives(&self.directives, f); 419 | format_inputs(&self.fields, f); 420 | } 421 | } 422 | 423 | impl<'a, T> Displayable for InputObjectTypeExtension<'a, T> 424 | where 425 | T: Text<'a>, 426 | { 427 | fn display(&self, f: &mut Formatter) { 428 | f.indent(); 429 | f.write("extend input "); 430 | f.write(self.name.as_ref()); 431 | format_directives(&self.directives, f); 432 | format_inputs(&self.fields, f); 433 | } 434 | } 435 | 436 | impl<'a, T> Displayable for TypeExtension<'a, T> 437 | where 438 | T: Text<'a>, 439 | { 440 | fn display(&self, f: &mut Formatter) { 441 | match *self { 442 | TypeExtension::Scalar(ref s) => s.display(f), 443 | TypeExtension::Object(ref o) => o.display(f), 444 | TypeExtension::Interface(ref i) => i.display(f), 445 | TypeExtension::Union(ref u) => u.display(f), 446 | TypeExtension::Enum(ref e) => e.display(f), 447 | TypeExtension::InputObject(ref i) => i.display(f), 448 | } 449 | } 450 | } 451 | 452 | impl<'a, T> Displayable for DirectiveDefinition<'a, T> 453 | where 454 | T: Text<'a>, 455 | { 456 | fn display(&self, f: &mut Formatter) { 457 | description(&self.description, f); 458 | f.indent(); 459 | f.write("directive @"); 460 | f.write(self.name.as_ref()); 461 | format_arguments(&self.arguments, f); 462 | if self.repeatable { 463 | f.write(" repeatable"); 464 | } 465 | if !self.locations.is_empty() { 466 | f.write(" on "); 467 | let mut first = true; 468 | for loc in &self.locations { 469 | if first { 470 | first = false; 471 | } else { 472 | f.write(" | "); 473 | } 474 | f.write(loc.as_str()); 475 | } 476 | } 477 | f.endline(); 478 | } 479 | } 480 | 481 | impl_display!( 482 | 'a 483 | Document, 484 | Definition, 485 | SchemaDefinition, 486 | TypeDefinition, 487 | TypeExtension, 488 | ScalarType, 489 | ScalarTypeExtension, 490 | ObjectType, 491 | ObjectTypeExtension, 492 | Field, 493 | InputValue, 494 | InterfaceType, 495 | InterfaceTypeExtension, 496 | UnionType, 497 | UnionTypeExtension, 498 | EnumType, 499 | EnumTypeExtension, 500 | InputObjectType, 501 | InputObjectTypeExtension, 502 | DirectiveDefinition, 503 | ); 504 | -------------------------------------------------------------------------------- /src/schema/grammar.rs: -------------------------------------------------------------------------------- 1 | use combine::easy::{Error, Errors}; 2 | use combine::error::StreamError; 3 | use combine::sep_by1; 4 | use combine::{choice, eof, many, many1, optional, position}; 5 | use combine::{parser, Parser, StdParseResult}; 6 | 7 | use crate::common::{default_value, directives, parse_type, string, Text}; 8 | use crate::helpers::{ident, kind, name, punct}; 9 | use crate::schema::ast::*; 10 | use crate::schema::error::ParseError; 11 | use crate::tokenizer::{Kind as T, Token, TokenStream}; 12 | 13 | pub fn schema<'a, S>( 14 | input: &mut TokenStream<'a>, 15 | ) -> StdParseResult, TokenStream<'a>> 16 | where 17 | S: Text<'a>, 18 | { 19 | ( 20 | position().skip(ident("schema")), 21 | parser(directives), 22 | punct("{") 23 | .with(many((kind(T::Name).skip(punct(":")), name::<'a, S>()))) 24 | .skip(punct("}")), 25 | ) 26 | .flat_map( 27 | |(position, directives, operations): (_, _, Vec<(Token, _)>)| { 28 | let mut query = None; 29 | let mut mutation = None; 30 | let mut subscription = None; 31 | let mut err = Errors::empty(position); 32 | for (oper, type_name) in operations { 33 | match oper.value { 34 | "query" if query.is_some() => { 35 | err.add_error(Error::unexpected_static_message( 36 | "duplicate `query` operation", 37 | )); 38 | } 39 | "query" => { 40 | query = Some(type_name); 41 | } 42 | "mutation" if mutation.is_some() => { 43 | err.add_error(Error::unexpected_static_message( 44 | "duplicate `mutation` operation", 45 | )); 46 | } 47 | "mutation" => { 48 | mutation = Some(type_name); 49 | } 50 | "subscription" if subscription.is_some() => { 51 | err.add_error(Error::unexpected_static_message( 52 | "duplicate `subscription` operation", 53 | )); 54 | } 55 | "subscription" => { 56 | subscription = Some(type_name); 57 | } 58 | _ => { 59 | err.add_error(Error::unexpected_token(oper)); 60 | err.add_error(Error::expected_static_message("query")); 61 | err.add_error(Error::expected_static_message("mutation")); 62 | err.add_error(Error::expected_static_message("subscription")); 63 | } 64 | } 65 | } 66 | if !err.errors.is_empty() { 67 | return Err(err); 68 | } 69 | Ok(SchemaDefinition { 70 | position, 71 | directives, 72 | query, 73 | mutation, 74 | subscription, 75 | }) 76 | }, 77 | ) 78 | .parse_stream(input) 79 | .into_result() 80 | } 81 | 82 | pub fn scalar_type<'a, T>( 83 | input: &mut TokenStream<'a>, 84 | ) -> StdParseResult, TokenStream<'a>> 85 | where 86 | T: Text<'a>, 87 | { 88 | ( 89 | position(), 90 | ident("scalar").with(name::<'a, T>()), 91 | parser(directives), 92 | ) 93 | .map(|(position, name, directives)| ScalarType { 94 | position, 95 | description: None, 96 | name, 97 | directives, 98 | }) 99 | .parse_stream(input) 100 | .into_result() 101 | } 102 | 103 | pub fn scalar_type_extension<'a, T>( 104 | input: &mut TokenStream<'a>, 105 | ) -> StdParseResult, TokenStream<'a>> 106 | where 107 | T: Text<'a>, 108 | { 109 | ( 110 | position(), 111 | ident("scalar").with(name::<'a, T>()), 112 | parser(directives), 113 | ) 114 | .flat_map(|(position, name, directives)| { 115 | if directives.is_empty() { 116 | let mut e = Errors::empty(position); 117 | e.add_error(Error::expected_static_message( 118 | "Scalar type extension should contain at least \ 119 | one directive.", 120 | )); 121 | return Err(e); 122 | } 123 | Ok(ScalarTypeExtension { 124 | position, 125 | name, 126 | directives, 127 | }) 128 | }) 129 | .parse_stream(input) 130 | .into_result() 131 | } 132 | 133 | pub fn implements_interfaces<'a, X>( 134 | input: &mut TokenStream<'a>, 135 | ) -> StdParseResult, TokenStream<'a>> 136 | where 137 | X: Text<'a>, 138 | { 139 | optional( 140 | ident("implements") 141 | .skip(optional(punct("&"))) 142 | .with(sep_by1(name::<'a, X>(), punct("&"))), 143 | ) 144 | .map(|opt| opt.unwrap_or_default()) 145 | .parse_stream(input) 146 | .into_result() 147 | } 148 | 149 | pub fn input_value<'a, X>( 150 | input: &mut TokenStream<'a>, 151 | ) -> StdParseResult, TokenStream<'a>> 152 | where 153 | X: Text<'a>, 154 | { 155 | ( 156 | position(), 157 | optional(parser(string)), 158 | name::<'a, X>(), 159 | punct(":").with(parser(parse_type)), 160 | optional(punct("=").with(parser(default_value))), 161 | parser(directives), 162 | ) 163 | .map( 164 | |(position, description, name, value_type, default_value, directives)| InputValue { 165 | position, 166 | description, 167 | name, 168 | value_type, 169 | default_value, 170 | directives, 171 | }, 172 | ) 173 | .parse_stream(input) 174 | .into_result() 175 | } 176 | 177 | pub fn arguments_definition<'a, T>( 178 | input: &mut TokenStream<'a>, 179 | ) -> StdParseResult>, TokenStream<'a>> 180 | where 181 | T: Text<'a>, 182 | { 183 | optional(punct("(").with(many1(parser(input_value))).skip(punct(")"))) 184 | .map(|v| v.unwrap_or_default()) 185 | .parse_stream(input) 186 | .into_result() 187 | } 188 | 189 | pub fn field<'a, S>(input: &mut TokenStream<'a>) -> StdParseResult, TokenStream<'a>> 190 | where 191 | S: Text<'a>, 192 | { 193 | ( 194 | position(), 195 | optional(parser(string)), 196 | name::<'a, S>(), 197 | parser(arguments_definition), 198 | punct(":").with(parser(parse_type)), 199 | parser(directives), 200 | ) 201 | .map( 202 | |(position, description, name, arguments, field_type, directives)| Field { 203 | position, 204 | description, 205 | name, 206 | arguments, 207 | field_type, 208 | directives, 209 | }, 210 | ) 211 | .parse_stream(input) 212 | .into_result() 213 | } 214 | 215 | pub fn fields<'a, S>( 216 | input: &mut TokenStream<'a>, 217 | ) -> StdParseResult>, TokenStream<'a>> 218 | where 219 | S: Text<'a>, 220 | { 221 | optional(punct("{").with(many1(parser(field))).skip(punct("}"))) 222 | .map(|v| v.unwrap_or_default()) 223 | .parse_stream(input) 224 | .into_result() 225 | } 226 | 227 | pub fn object_type<'a, S>( 228 | input: &mut TokenStream<'a>, 229 | ) -> StdParseResult, TokenStream<'a>> 230 | where 231 | S: Text<'a>, 232 | { 233 | ( 234 | position(), 235 | ident("type").with(name::<'a, S>()), 236 | parser(implements_interfaces::), 237 | parser(directives), 238 | parser(fields), 239 | ) 240 | .map(|(position, name, interfaces, directives, fields)| { 241 | ObjectType { 242 | position, 243 | name, 244 | directives, 245 | fields, 246 | implements_interfaces: interfaces, 247 | description: None, // is filled in described_definition 248 | } 249 | }) 250 | .parse_stream(input) 251 | .into_result() 252 | } 253 | 254 | pub fn object_type_extension<'a, S>( 255 | input: &mut TokenStream<'a>, 256 | ) -> StdParseResult, TokenStream<'a>> 257 | where 258 | S: Text<'a>, 259 | { 260 | ( 261 | position(), 262 | ident("type").with(name::<'a, S>()), 263 | parser(implements_interfaces::), 264 | parser(directives), 265 | parser(fields), 266 | ) 267 | .flat_map(|(position, name, interfaces, directives, fields)| { 268 | if interfaces.is_empty() && directives.is_empty() && fields.is_empty() { 269 | let mut e = Errors::empty(position); 270 | e.add_error(Error::expected_static_message( 271 | "Object type extension should contain at least \ 272 | one interface, directive or field.", 273 | )); 274 | return Err(e); 275 | } 276 | Ok(ObjectTypeExtension { 277 | position, 278 | name, 279 | directives, 280 | fields, 281 | implements_interfaces: interfaces, 282 | }) 283 | }) 284 | .parse_stream(input) 285 | .into_result() 286 | } 287 | 288 | pub fn interface_type<'a, T>( 289 | input: &mut TokenStream<'a>, 290 | ) -> StdParseResult, TokenStream<'a>> 291 | where 292 | T: Text<'a>, 293 | { 294 | ( 295 | position(), 296 | ident("interface").with(name::<'a, T>()), 297 | parser(implements_interfaces::), 298 | parser(directives), 299 | parser(fields), 300 | ) 301 | .map(|(position, name, interfaces, directives, fields)| { 302 | InterfaceType { 303 | position, 304 | name, 305 | implements_interfaces: interfaces, 306 | directives, 307 | fields, 308 | description: None, // is filled in described_definition 309 | } 310 | }) 311 | .parse_stream(input) 312 | .into_result() 313 | } 314 | 315 | pub fn interface_type_extension<'a, T>( 316 | input: &mut TokenStream<'a>, 317 | ) -> StdParseResult, TokenStream<'a>> 318 | where 319 | T: Text<'a>, 320 | { 321 | ( 322 | position(), 323 | ident("interface").with(name::<'a, T>()), 324 | parser(implements_interfaces::), 325 | parser(directives), 326 | parser(fields), 327 | ) 328 | .flat_map(|(position, name, interfaces, directives, fields)| { 329 | if directives.is_empty() && fields.is_empty() { 330 | let mut e = Errors::empty(position); 331 | e.add_error(Error::expected_static_message( 332 | "Interface type extension should contain at least \ 333 | one directive or field.", 334 | )); 335 | return Err(e); 336 | } 337 | Ok(InterfaceTypeExtension { 338 | position, 339 | name, 340 | implements_interfaces: interfaces, 341 | directives, 342 | fields, 343 | }) 344 | }) 345 | .parse_stream(input) 346 | .into_result() 347 | } 348 | 349 | pub fn union_members<'a, T>( 350 | input: &mut TokenStream<'a>, 351 | ) -> StdParseResult, TokenStream<'a>> 352 | where 353 | T: Text<'a>, 354 | { 355 | optional(punct("|")) 356 | .with(sep_by1(name::<'a, T>(), punct("|"))) 357 | .parse_stream(input) 358 | .into_result() 359 | } 360 | 361 | pub fn union_type<'a, T>( 362 | input: &mut TokenStream<'a>, 363 | ) -> StdParseResult, TokenStream<'a>> 364 | where 365 | T: Text<'a>, 366 | { 367 | ( 368 | position(), 369 | ident("union").with(name::<'a, T>()), 370 | parser(directives), 371 | optional(punct("=").with(parser(union_members::))), 372 | ) 373 | .map(|(position, name, directives, types)| { 374 | UnionType { 375 | position, 376 | name, 377 | directives, 378 | types: types.unwrap_or_else(Vec::new), 379 | description: None, // is filled in described_definition 380 | } 381 | }) 382 | .parse_stream(input) 383 | .into_result() 384 | } 385 | 386 | pub fn union_type_extension<'a, T>( 387 | input: &mut TokenStream<'a>, 388 | ) -> StdParseResult, TokenStream<'a>> 389 | where 390 | T: Text<'a>, 391 | { 392 | ( 393 | position(), 394 | ident("union").with(name::<'a, T>()), 395 | parser(directives), 396 | optional(punct("=").with(parser(union_members::))), 397 | ) 398 | .flat_map(|(position, name, directives, types)| { 399 | if directives.is_empty() && types.is_none() { 400 | let mut e = Errors::empty(position); 401 | e.add_error(Error::expected_static_message( 402 | "Union type extension should contain at least \ 403 | one directive or type.", 404 | )); 405 | return Err(e); 406 | } 407 | Ok(UnionTypeExtension { 408 | position, 409 | name, 410 | directives, 411 | types: types.unwrap_or_else(Vec::new), 412 | }) 413 | }) 414 | .parse_stream(input) 415 | .into_result() 416 | } 417 | 418 | pub fn enum_values<'a, T>( 419 | input: &mut TokenStream<'a>, 420 | ) -> StdParseResult>, TokenStream<'a>> 421 | where 422 | T: Text<'a>, 423 | { 424 | punct("{") 425 | .with(many1( 426 | ( 427 | position(), 428 | optional(parser(string)), 429 | name::<'a, T>(), 430 | parser(directives), 431 | ) 432 | .map(|(position, description, name, directives)| EnumValue { 433 | position, 434 | description, 435 | name, 436 | directives, 437 | }), 438 | )) 439 | .skip(punct("}")) 440 | .parse_stream(input) 441 | .into_result() 442 | } 443 | 444 | pub fn enum_type<'a, T>( 445 | input: &mut TokenStream<'a>, 446 | ) -> StdParseResult, TokenStream<'a>> 447 | where 448 | T: Text<'a>, 449 | { 450 | ( 451 | position(), 452 | ident("enum").with(name::<'a, T>()), 453 | parser(directives), 454 | optional(parser(enum_values)), 455 | ) 456 | .map(|(position, name, directives, values)| { 457 | EnumType { 458 | position, 459 | name, 460 | directives, 461 | values: values.unwrap_or_else(Vec::new), 462 | description: None, // is filled in described_definition 463 | } 464 | }) 465 | .parse_stream(input) 466 | .into_result() 467 | } 468 | 469 | pub fn enum_type_extension<'a, T>( 470 | input: &mut TokenStream<'a>, 471 | ) -> StdParseResult, TokenStream<'a>> 472 | where 473 | T: Text<'a>, 474 | { 475 | ( 476 | position(), 477 | ident("enum").with(name::<'a, T>()), 478 | parser(directives), 479 | optional(parser(enum_values)), 480 | ) 481 | .flat_map(|(position, name, directives, values)| { 482 | if directives.is_empty() && values.is_none() { 483 | let mut e = Errors::empty(position); 484 | e.add_error(Error::expected_static_message( 485 | "Enum type extension should contain at least \ 486 | one directive or value.", 487 | )); 488 | return Err(e); 489 | } 490 | Ok(EnumTypeExtension { 491 | position, 492 | name, 493 | directives, 494 | values: values.unwrap_or_else(Vec::new), 495 | }) 496 | }) 497 | .parse_stream(input) 498 | .into_result() 499 | } 500 | 501 | pub fn input_fields<'a, T>( 502 | input: &mut TokenStream<'a>, 503 | ) -> StdParseResult>, TokenStream<'a>> 504 | where 505 | T: Text<'a>, 506 | { 507 | optional(punct("{").with(many1(parser(input_value))).skip(punct("}"))) 508 | .map(|v| v.unwrap_or_default()) 509 | .parse_stream(input) 510 | .into_result() 511 | } 512 | 513 | pub fn input_object_type<'a, T>( 514 | input: &mut TokenStream<'a>, 515 | ) -> StdParseResult, TokenStream<'a>> 516 | where 517 | T: Text<'a>, 518 | { 519 | ( 520 | position(), 521 | ident("input").with(name::<'a, T>()), 522 | parser(directives), 523 | parser(input_fields), 524 | ) 525 | .map(|(position, name, directives, fields)| { 526 | InputObjectType { 527 | position, 528 | name, 529 | directives, 530 | fields, 531 | description: None, // is filled in described_definition 532 | } 533 | }) 534 | .parse_stream(input) 535 | .into_result() 536 | } 537 | 538 | pub fn input_object_type_extension<'a, T>( 539 | input: &mut TokenStream<'a>, 540 | ) -> StdParseResult, TokenStream<'a>> 541 | where 542 | T: Text<'a>, 543 | { 544 | ( 545 | position(), 546 | ident("input").with(name::<'a, T>()), 547 | parser(directives), 548 | parser(input_fields), 549 | ) 550 | .flat_map(|(position, name, directives, fields)| { 551 | if directives.is_empty() && fields.is_empty() { 552 | let mut e = Errors::empty(position); 553 | e.add_error(Error::expected_static_message( 554 | "Input object type extension should contain at least \ 555 | one directive or field.", 556 | )); 557 | return Err(e); 558 | } 559 | Ok(InputObjectTypeExtension { 560 | position, 561 | name, 562 | directives, 563 | fields, 564 | }) 565 | }) 566 | .parse_stream(input) 567 | .into_result() 568 | } 569 | 570 | pub fn directive_locations<'a>( 571 | input: &mut TokenStream<'a>, 572 | ) -> StdParseResult, TokenStream<'a>> { 573 | optional(optional(punct("|")).with(sep_by1( 574 | kind(T::Name).and_then(|tok| tok.value.parse::()), 575 | punct("|"), 576 | ))) 577 | .map(|opt| opt.unwrap_or_default()) 578 | .parse_stream(input) 579 | .into_result() 580 | } 581 | 582 | pub fn directive_definition<'a, T>( 583 | input: &mut TokenStream<'a>, 584 | ) -> StdParseResult, TokenStream<'a>> 585 | where 586 | T: Text<'a>, 587 | { 588 | ( 589 | position(), 590 | ident("directive").and(punct("@")).with(name::<'a, T>()), 591 | parser(arguments_definition), 592 | optional(ident("repeatable")), 593 | ident("on").with(parser(directive_locations)), 594 | ) 595 | .map(|(position, name, arguments, repeatable, locations)| { 596 | DirectiveDefinition { 597 | position, 598 | name, 599 | arguments, 600 | locations, 601 | repeatable: repeatable.is_some(), 602 | description: None, // is filled in described_definition 603 | } 604 | }) 605 | .parse_stream(input) 606 | .into_result() 607 | } 608 | 609 | pub fn described_definition<'a, T>( 610 | input: &mut TokenStream<'a>, 611 | ) -> StdParseResult, TokenStream<'a>> 612 | where 613 | T: Text<'a>, 614 | { 615 | use self::TypeDefinition::*; 616 | ( 617 | optional(parser(string)), 618 | choice(( 619 | choice(( 620 | parser(scalar_type).map(Scalar), 621 | parser(object_type).map(Object), 622 | parser(interface_type).map(Interface), 623 | parser(union_type).map(Union), 624 | parser(enum_type).map(Enum), 625 | parser(input_object_type).map(InputObject), 626 | )) 627 | .map(Definition::TypeDefinition), 628 | parser(directive_definition).map(Definition::DirectiveDefinition), 629 | )), 630 | ) 631 | // We can't set description inside type definition parser, because 632 | // that means parser will need to backtrace, and that in turn 633 | // means that error reporting is bad (along with performance) 634 | .map(|(descr, mut def)| { 635 | use crate::schema::ast::Definition::TypeDefinition as T; 636 | use crate::schema::ast::Definition::*; 637 | use crate::schema::ast::TypeDefinition::*; 638 | match def { 639 | T(Scalar(ref mut s)) => s.description = descr, 640 | T(Object(ref mut o)) => o.description = descr, 641 | T(Interface(ref mut i)) => i.description = descr, 642 | T(Union(ref mut u)) => u.description = descr, 643 | T(Enum(ref mut e)) => e.description = descr, 644 | T(InputObject(ref mut o)) => o.description = descr, 645 | DirectiveDefinition(ref mut d) => d.description = descr, 646 | SchemaDefinition(_) => unreachable!(), 647 | TypeExtension(_) => unreachable!(), 648 | } 649 | def 650 | }) 651 | .parse_stream(input) 652 | .into_result() 653 | } 654 | 655 | pub fn type_extension<'a, T>( 656 | input: &mut TokenStream<'a>, 657 | ) -> StdParseResult, TokenStream<'a>> 658 | where 659 | T: Text<'a>, 660 | { 661 | ident("extend") 662 | .with(choice(( 663 | parser(scalar_type_extension).map(TypeExtension::Scalar), 664 | parser(object_type_extension).map(TypeExtension::Object), 665 | parser(interface_type_extension).map(TypeExtension::Interface), 666 | parser(union_type_extension).map(TypeExtension::Union), 667 | parser(enum_type_extension).map(TypeExtension::Enum), 668 | parser(input_object_type_extension).map(TypeExtension::InputObject), 669 | ))) 670 | .parse_stream(input) 671 | .into_result() 672 | } 673 | 674 | pub fn definition<'a, T>( 675 | input: &mut TokenStream<'a>, 676 | ) -> StdParseResult, TokenStream<'a>> 677 | where 678 | T: Text<'a>, 679 | { 680 | choice(( 681 | parser(schema).map(Definition::SchemaDefinition), 682 | parser(type_extension).map(Definition::TypeExtension), 683 | parser(described_definition), 684 | )) 685 | .parse_stream(input) 686 | .into_result() 687 | } 688 | 689 | /// Parses a piece of schema language and returns an AST 690 | pub fn parse_schema<'a, T>(s: &'a str) -> Result, ParseError> 691 | where 692 | T: Text<'a>, 693 | { 694 | let mut tokens = TokenStream::new(s); 695 | let (doc, _) = many1(parser(definition)) 696 | .map(|d| Document { definitions: d }) 697 | .skip(eof()) 698 | .parse_stream(&mut tokens) 699 | .into_result() 700 | .map_err(|e| e.into_inner().error)?; 701 | 702 | Ok(doc) 703 | } 704 | 705 | #[cfg(test)] 706 | mod test { 707 | use super::parse_schema; 708 | use crate::position::Pos; 709 | use crate::schema::grammar::*; 710 | 711 | fn ast(s: &str) -> Document { 712 | parse_schema::(s).unwrap().to_owned() 713 | } 714 | 715 | #[test] 716 | fn one_field() { 717 | assert_eq!( 718 | ast("schema { query: Query }"), 719 | Document { 720 | definitions: vec![Definition::SchemaDefinition(SchemaDefinition { 721 | position: Pos { line: 1, column: 1 }, 722 | directives: vec![], 723 | query: Some("Query".into()), 724 | mutation: None, 725 | subscription: None 726 | })], 727 | } 728 | ); 729 | } 730 | } 731 | -------------------------------------------------------------------------------- /src/schema/mod.rs: -------------------------------------------------------------------------------- 1 | //! Schema definition language AST and utility 2 | //! 3 | mod ast; 4 | mod error; 5 | mod format; 6 | mod grammar; 7 | 8 | pub use self::ast::*; 9 | pub use self::error::ParseError; 10 | pub use self::grammar::parse_schema; 11 | -------------------------------------------------------------------------------- /src/tokenizer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use combine::easy::{Error, Errors, Info}; 4 | use combine::error::StreamError; 5 | use combine::stream::ResetStream; 6 | use combine::{Positioned, StreamOnce}; 7 | 8 | use crate::position::Pos; 9 | 10 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 11 | pub enum Kind { 12 | Punctuator, 13 | Name, 14 | IntValue, 15 | FloatValue, 16 | StringValue, 17 | BlockString, 18 | } 19 | 20 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 21 | pub struct Token<'a> { 22 | pub kind: Kind, 23 | pub value: &'a str, 24 | } 25 | 26 | #[derive(Debug, PartialEq)] 27 | pub struct TokenStream<'a> { 28 | buf: &'a str, 29 | position: Pos, 30 | off: usize, 31 | next_state: Option<(usize, Token<'a>, usize, Pos)>, 32 | recursion_limit: usize, 33 | } 34 | 35 | impl TokenStream<'_> { 36 | pub(crate) fn offset(&self) -> usize { 37 | self.off 38 | } 39 | } 40 | 41 | #[derive(Clone, Debug, PartialEq)] 42 | pub struct Checkpoint { 43 | position: Pos, 44 | off: usize, 45 | } 46 | 47 | impl<'a> StreamOnce for TokenStream<'a> { 48 | type Token = Token<'a>; 49 | type Range = Token<'a>; 50 | type Position = Pos; 51 | type Error = Errors, Token<'a>, Pos>; 52 | 53 | fn uncons(&mut self) -> Result, Token<'a>>> { 54 | if let Some((at, tok, off, pos)) = self.next_state { 55 | if at == self.off { 56 | self.off = off; 57 | self.position = pos; 58 | return Ok(tok); 59 | } 60 | } 61 | let old_pos = self.off; 62 | let (kind, len) = self.take_token()?; 63 | let value = &self.buf[self.off - len..self.off]; 64 | self.skip_whitespace(); 65 | let token = Token { kind, value }; 66 | self.next_state = Some((old_pos, token, self.off, self.position)); 67 | Ok(token) 68 | } 69 | } 70 | 71 | impl<'a> Positioned for TokenStream<'a> { 72 | fn position(&self) -> Self::Position { 73 | self.position 74 | } 75 | } 76 | 77 | impl<'a> ResetStream for TokenStream<'a> { 78 | type Checkpoint = Checkpoint; 79 | fn checkpoint(&self) -> Self::Checkpoint { 80 | Checkpoint { 81 | position: self.position, 82 | off: self.off, 83 | } 84 | } 85 | fn reset(&mut self, checkpoint: Checkpoint) -> Result<(), Self::Error> { 86 | self.position = checkpoint.position; 87 | self.off = checkpoint.off; 88 | Ok(()) 89 | } 90 | } 91 | 92 | // NOTE: we expect that first character is always digit or minus, as returned 93 | // by tokenizer 94 | fn check_int(value: &str) -> bool { 95 | value == "0" 96 | || value == "-0" 97 | || (!value.starts_with('0') 98 | && value != "-" 99 | && !value.starts_with("-0") 100 | && value[1..].chars().all(|x| x.is_ascii_digit())) 101 | } 102 | 103 | fn check_dec(value: &str) -> bool { 104 | !value.is_empty() && value.chars().all(|x| x.is_ascii_digit()) 105 | } 106 | 107 | fn check_exp(value: &str) -> bool { 108 | if value.is_empty() { 109 | return false; 110 | } 111 | let first = value.chars().next().unwrap(); 112 | if first != '-' && first != '+' && (first <= '0' || first >= '9') { 113 | return false; 114 | } 115 | 116 | value[1..].chars().all(|x| x.is_ascii_digit()) 117 | } 118 | 119 | fn check_float(value: &str, exponent: Option, real: Option) -> bool { 120 | match (exponent, real) { 121 | (Some(e), Some(r)) if e < r => false, 122 | (Some(e), Some(r)) => { 123 | check_int(&value[..r]) && check_dec(&value[r + 1..e]) && check_exp(&value[e + 1..]) 124 | } 125 | (Some(e), None) => check_int(&value[..e]) && check_exp(&value[e + 1..]), 126 | (None, Some(r)) => check_int(&value[..r]) && check_dec(&value[r + 1..]), 127 | (None, None) => unreachable!(), 128 | } 129 | } 130 | 131 | impl<'a> TokenStream<'a> { 132 | pub fn new(s: &str) -> TokenStream { 133 | Self::with_recursion_limit(s, 50) 134 | } 135 | 136 | /// Specify a limit to recursive parsing. Note that increasing the limit 137 | /// from the default may represent a security issue since a maliciously 138 | /// crafted input may cause a stack overflow, crashing the process. 139 | pub(crate) fn with_recursion_limit(s: &str, recursion_limit: usize) -> TokenStream { 140 | let mut me = TokenStream { 141 | buf: s, 142 | position: Pos { line: 1, column: 1 }, 143 | off: 0, 144 | next_state: None, 145 | recursion_limit, 146 | }; 147 | me.skip_whitespace(); 148 | me 149 | } 150 | 151 | /// Convenience for the common case where a token does 152 | /// not span multiple lines. Infallible. 153 | #[inline] 154 | fn advance_token(&mut self, kind: Kind, size: usize) -> Result<(Kind, usize), T> { 155 | self.position.column += size; 156 | self.off += size; 157 | Ok((kind, size)) 158 | } 159 | 160 | fn take_token(&mut self) -> Result<(Kind, usize), Error, Token<'a>>> { 161 | use self::Kind::*; 162 | let mut iter = self.buf[self.off..].char_indices(); 163 | let cur_char = match iter.next() { 164 | Some((_, x)) => x, 165 | None => return Err(Error::end_of_input()), 166 | }; 167 | 168 | match cur_char { 169 | '(' | '[' | '{' => { 170 | // Check for recursion limit 171 | self.recursion_limit = self 172 | .recursion_limit 173 | .checked_sub(1) 174 | .ok_or_else(|| Error::message_static_message("Recursion limit exceeded"))?; 175 | 176 | self.advance_token(Punctuator, 1) 177 | } 178 | ')' | ']' | '}' => { 179 | // Notes on exceptional cases: 180 | // recursion_limit may exceed the original value specified 181 | // when constructing the Tokenizer. It may at first 182 | // seem like this would be a good place to handle that, 183 | // but instead this code allows this token to propagate up 184 | // to the parser which is better equipped to make specific 185 | // error messages about unmatched pairs. 186 | // The case where recursion limit would overflow but instead 187 | // saturates is just a specific case of the more general 188 | // occurrence above. 189 | self.recursion_limit = self.recursion_limit.saturating_add(1); 190 | self.advance_token(Punctuator, 1) 191 | } 192 | '!' | '$' | ':' | '=' | '@' | '|' | '&' => self.advance_token(Punctuator, 1), 193 | '.' => { 194 | if iter.as_str().starts_with("..") { 195 | self.advance_token(Punctuator, 3) 196 | } else { 197 | Err(Error::Unexpected(Info::Owned( 198 | format_args!( 199 | "bare dot {:?} is not supported, \ 200 | only \"...\"", 201 | cur_char 202 | ) 203 | .to_string(), 204 | ))) 205 | } 206 | } 207 | '_' | 'a'..='z' | 'A'..='Z' => { 208 | for (idx, cur_char) in iter.by_ref() { 209 | match cur_char { 210 | '_' | 'a'..='z' | 'A'..='Z' | '0'..='9' => continue, 211 | _ => return self.advance_token(Name, idx), 212 | } 213 | } 214 | let len = self.buf.len() - self.off; 215 | self.position.column += len; 216 | self.off += len; 217 | 218 | Ok((Name, len)) 219 | } 220 | '-' | '0'..='9' => { 221 | let mut exponent = None; 222 | let mut real = None; 223 | let len = loop { 224 | let (idx, cur_char) = match iter.next() { 225 | Some(pair) => pair, 226 | None => break self.buf.len() - self.off, 227 | }; 228 | match cur_char { 229 | // just scan for now, will validate later on 230 | ' ' | '\n' | '\r' | '\t' | ',' | '#' | '!' | '$' | ':' | '=' | '@' 231 | | '|' | '&' | '(' | ')' | '[' | ']' | '{' | '}' => break idx, 232 | '.' => real = Some(idx), 233 | 'e' | 'E' => exponent = Some(idx), 234 | _ => {} 235 | } 236 | }; 237 | 238 | if exponent.is_some() || real.is_some() { 239 | let value = &self.buf[self.off..][..len]; 240 | if !check_float(value, exponent, real) { 241 | return Err(Error::Unexpected(Info::Owned( 242 | format_args!("unsupported float {:?}", value).to_string(), 243 | ))); 244 | } 245 | self.position.column += len; 246 | self.off += len; 247 | 248 | Ok((FloatValue, len)) 249 | } else { 250 | let value = &self.buf[self.off..][..len]; 251 | if !check_int(value) { 252 | return Err(Error::Unexpected(Info::Owned( 253 | format_args!("unsupported integer {:?}", value).to_string(), 254 | ))); 255 | } 256 | self.advance_token(IntValue, len) 257 | } 258 | } 259 | '"' => { 260 | if iter.as_str().starts_with("\"\"") { 261 | let tail = &iter.as_str()[2..]; 262 | for (end_idx, _) in tail.match_indices("\"\"\"") { 263 | if !tail[..end_idx].ends_with('\\') { 264 | self.update_position(end_idx + 6); 265 | return Ok((BlockString, end_idx + 6)); 266 | } 267 | } 268 | 269 | Err(Error::Unexpected(Info::Owned( 270 | "unterminated block string value".to_string(), 271 | ))) 272 | } else { 273 | let mut nchars = 1; 274 | let mut escaped = false; 275 | for (idx, cur_char) in iter { 276 | nchars += 1; 277 | match cur_char { 278 | '"' if escaped => {} 279 | '"' => { 280 | self.position.column += nchars; 281 | self.off += idx + 1; 282 | return Ok((StringValue, idx + 1)); 283 | } 284 | '\n' => { 285 | return Err(Error::Unexpected(Info::Owned( 286 | "unterminated string value".to_string(), 287 | ))); 288 | } 289 | 290 | _ => {} 291 | } 292 | 293 | // if we aren't escaped and the current char is a \, we are now escaped 294 | escaped = !escaped && cur_char == '\\'; 295 | } 296 | Err(Error::Unexpected(Info::Owned( 297 | "unterminated string value".to_string(), 298 | ))) 299 | } 300 | } 301 | _ => Err(Error::Unexpected(Info::Owned( 302 | format_args!("unexpected character {:?}", cur_char).to_string(), 303 | ))), 304 | } 305 | } 306 | 307 | fn skip_whitespace(&mut self) { 308 | let mut iter = self.buf[self.off..].char_indices(); 309 | let idx = loop { 310 | let (idx, cur_char) = match iter.next() { 311 | Some(pair) => pair, 312 | None => break self.buf.len() - self.off, 313 | }; 314 | match cur_char { 315 | '\u{feff}' | '\r' => continue, 316 | '\t' => self.position.column += 8, 317 | '\n' => { 318 | self.position.column = 1; 319 | self.position.line += 1; 320 | } 321 | // comma is also entirely ignored in spec 322 | ' ' | ',' => { 323 | self.position.column += 1; 324 | continue; 325 | } 326 | //comment 327 | '#' => { 328 | for (_, cur_char) in iter.by_ref() { 329 | // TODO(tailhook) ensure SourceCharacter 330 | if cur_char == '\r' || cur_char == '\n' { 331 | self.position.column = 1; 332 | self.position.line += 1; 333 | break; 334 | } 335 | } 336 | continue; 337 | } 338 | _ => break idx, 339 | } 340 | }; 341 | self.off += idx; 342 | } 343 | 344 | fn update_position(&mut self, len: usize) { 345 | let val = &self.buf[self.off..][..len]; 346 | self.off += len; 347 | let lines = val.as_bytes().iter().filter(|&&x| x == b'\n').count(); 348 | self.position.line += lines; 349 | if lines > 0 { 350 | let line_offset = val.rfind('\n').unwrap() + 1; 351 | let num = val[line_offset..].chars().count(); 352 | self.position.column = num + 1; 353 | } else { 354 | let num = val.chars().count(); 355 | self.position.column += num; 356 | } 357 | } 358 | } 359 | 360 | impl<'a> fmt::Display for Token<'a> { 361 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 362 | write!(f, "{}[{:?}]", self.value, self.kind) 363 | } 364 | } 365 | 366 | #[cfg(test)] 367 | mod test { 368 | use super::Kind::*; 369 | use super::{Kind, TokenStream}; 370 | use combine::easy::Error; 371 | 372 | use combine::{Positioned, StreamOnce}; 373 | 374 | fn tok_str(s: &str) -> Vec<&str> { 375 | let mut r = Vec::new(); 376 | let mut s = TokenStream::new(s); 377 | loop { 378 | match s.uncons() { 379 | Ok(x) => r.push(x.value), 380 | Err(ref e) if e == &Error::end_of_input() => break, 381 | Err(e) => panic!("Parse error at {}: {}", s.position(), e), 382 | } 383 | } 384 | r 385 | } 386 | fn tok_typ(s: &str) -> Vec { 387 | let mut r = Vec::new(); 388 | let mut s = TokenStream::new(s); 389 | loop { 390 | match s.uncons() { 391 | Ok(x) => r.push(x.kind), 392 | Err(ref e) if e == &Error::end_of_input() => break, 393 | Err(e) => panic!("Parse error at {}: {}", s.position(), e), 394 | } 395 | } 396 | r 397 | } 398 | 399 | #[test] 400 | fn comments_and_commas() { 401 | assert_eq!(tok_str("# hello { world }"), &[] as &[&str]); 402 | assert_eq!(tok_str("# x\n,,,"), &[] as &[&str]); 403 | assert_eq!(tok_str(", ,, ,,, # x"), &[] as &[&str]); 404 | } 405 | 406 | #[test] 407 | fn simple() { 408 | assert_eq!(tok_str("a { b }"), ["a", "{", "b", "}"]); 409 | assert_eq!(tok_typ("a { b }"), [Name, Punctuator, Name, Punctuator]); 410 | } 411 | 412 | #[test] 413 | fn query() { 414 | assert_eq!( 415 | tok_str( 416 | "query Query { 417 | object { field } 418 | }" 419 | ), 420 | ["query", "Query", "{", "object", "{", "field", "}", "}"] 421 | ); 422 | } 423 | 424 | #[test] 425 | fn fragment() { 426 | assert_eq!(tok_str("a { ...b }"), ["a", "{", "...", "b", "}"]); 427 | } 428 | 429 | #[test] 430 | fn int() { 431 | assert_eq!(tok_str("0"), ["0"]); 432 | assert_eq!(tok_str("0,"), ["0"]); 433 | assert_eq!(tok_str("0# x"), ["0"]); 434 | assert_eq!(tok_typ("0"), [IntValue]); 435 | assert_eq!(tok_str("-0"), ["-0"]); 436 | assert_eq!(tok_typ("-0"), [IntValue]); 437 | assert_eq!(tok_str("-1"), ["-1"]); 438 | assert_eq!(tok_typ("-1"), [IntValue]); 439 | assert_eq!(tok_str("-132"), ["-132"]); 440 | assert_eq!(tok_typ("-132"), [IntValue]); 441 | assert_eq!(tok_str("132"), ["132"]); 442 | assert_eq!(tok_typ("132"), [IntValue]); 443 | assert_eq!( 444 | tok_str("a(x: 10) { b }"), 445 | ["a", "(", "x", ":", "10", ")", "{", "b", "}"] 446 | ); 447 | assert_eq!( 448 | tok_typ("a(x: 10) { b }"), 449 | [ 450 | Name, Punctuator, Name, Punctuator, IntValue, Punctuator, Punctuator, Name, 451 | Punctuator 452 | ] 453 | ); 454 | } 455 | 456 | // TODO(tailhook) fix errors in parser and check error message 457 | #[test] 458 | #[should_panic] 459 | fn zero_int() { 460 | tok_str("01"); 461 | } 462 | #[test] 463 | #[should_panic] 464 | fn zero_int4() { 465 | tok_str("00001"); 466 | } 467 | #[test] 468 | #[should_panic] 469 | fn minus_int() { 470 | tok_str("-"); 471 | } 472 | #[test] 473 | #[should_panic] 474 | fn minus_zero_int() { 475 | tok_str("-01"); 476 | } 477 | #[test] 478 | #[should_panic] 479 | fn minus_zero_int4() { 480 | tok_str("-00001"); 481 | } 482 | #[test] 483 | #[should_panic] 484 | fn letters_int() { 485 | tok_str("0bbc"); 486 | } 487 | 488 | #[test] 489 | fn float() { 490 | assert_eq!(tok_str("0.0"), ["0.0"]); 491 | assert_eq!(tok_typ("0.0"), [FloatValue]); 492 | assert_eq!(tok_str("-0.0"), ["-0.0"]); 493 | assert_eq!(tok_typ("-0.0"), [FloatValue]); 494 | assert_eq!(tok_str("-1.0"), ["-1.0"]); 495 | assert_eq!(tok_typ("-1.0"), [FloatValue]); 496 | assert_eq!(tok_str("-1.023"), ["-1.023"]); 497 | assert_eq!(tok_typ("-1.023"), [FloatValue]); 498 | assert_eq!(tok_str("-132.0"), ["-132.0"]); 499 | assert_eq!(tok_typ("-132.0"), [FloatValue]); 500 | assert_eq!(tok_str("132.0"), ["132.0"]); 501 | assert_eq!(tok_typ("132.0"), [FloatValue]); 502 | assert_eq!(tok_str("0e+0"), ["0e+0"]); 503 | assert_eq!(tok_typ("0e+0"), [FloatValue]); 504 | assert_eq!(tok_str("0.0e+0"), ["0.0e+0"]); 505 | assert_eq!(tok_typ("0.0e+0"), [FloatValue]); 506 | assert_eq!(tok_str("-0e+0"), ["-0e+0"]); 507 | assert_eq!(tok_typ("-0e+0"), [FloatValue]); 508 | assert_eq!(tok_str("-1e+0"), ["-1e+0"]); 509 | assert_eq!(tok_typ("-1e+0"), [FloatValue]); 510 | assert_eq!(tok_str("-132e+0"), ["-132e+0"]); 511 | assert_eq!(tok_typ("-132e+0"), [FloatValue]); 512 | assert_eq!(tok_str("132e+0"), ["132e+0"]); 513 | assert_eq!(tok_typ("132e+0"), [FloatValue]); 514 | assert_eq!( 515 | tok_str("a(x: 10.0) { b }"), 516 | ["a", "(", "x", ":", "10.0", ")", "{", "b", "}"] 517 | ); 518 | assert_eq!( 519 | tok_typ("a(x: 10.0) { b }"), 520 | [ 521 | Name, Punctuator, Name, Punctuator, FloatValue, Punctuator, Punctuator, Name, 522 | Punctuator 523 | ] 524 | ); 525 | assert_eq!(tok_str("1.23e4"), ["1.23e4"]); 526 | assert_eq!(tok_typ("1.23e4"), [FloatValue]); 527 | } 528 | 529 | // TODO(tailhook) fix errors in parser and check error message 530 | #[test] 531 | #[should_panic] 532 | fn no_int_float() { 533 | tok_str(".0"); 534 | } 535 | #[test] 536 | #[should_panic] 537 | fn no_int_float1() { 538 | tok_str(".1"); 539 | } 540 | #[test] 541 | #[should_panic] 542 | fn zero_float() { 543 | tok_str("01.0"); 544 | } 545 | #[test] 546 | #[should_panic] 547 | fn zero_float4() { 548 | tok_str("00001.0"); 549 | } 550 | #[test] 551 | #[should_panic] 552 | fn minus_float() { 553 | tok_str("-.0"); 554 | } 555 | #[test] 556 | #[should_panic] 557 | fn minus_zero_float() { 558 | tok_str("-01.0"); 559 | } 560 | #[test] 561 | #[should_panic] 562 | fn minus_zero_float4() { 563 | tok_str("-00001.0"); 564 | } 565 | #[test] 566 | #[should_panic] 567 | fn letters_float() { 568 | tok_str("0bbc.0"); 569 | } 570 | #[test] 571 | #[should_panic] 572 | fn letters_float2() { 573 | tok_str("0.bbc"); 574 | } 575 | #[test] 576 | #[should_panic] 577 | fn letters_float3() { 578 | tok_str("0.bbce0"); 579 | } 580 | #[test] 581 | #[should_panic] 582 | fn no_exp_sign_float() { 583 | tok_str("0e0"); 584 | } 585 | #[test] 586 | #[should_panic] 587 | fn unterminated_string() { 588 | tok_str(r#""hello\""#); 589 | } 590 | #[test] 591 | #[should_panic] 592 | fn extra_unterminated_string() { 593 | tok_str(r#""hello\\\""#); 594 | } 595 | 596 | #[test] 597 | fn string() { 598 | assert_eq!(tok_str(r#""""#), [r#""""#]); 599 | assert_eq!(tok_typ(r#""""#), [StringValue]); 600 | assert_eq!(tok_str(r#""hello""#), [r#""hello""#]); 601 | assert_eq!(tok_str(r#""hello\\""#), [r#""hello\\""#]); 602 | assert_eq!(tok_str(r#""hello\\\\""#), [r#""hello\\\\""#]); 603 | assert_eq!(tok_str(r#""he\\llo""#), [r#""he\\llo""#]); 604 | assert_eq!(tok_typ(r#""hello""#), [StringValue]); 605 | assert_eq!(tok_str(r#""my\"quote""#), [r#""my\"quote""#]); 606 | assert_eq!(tok_typ(r#""my\"quote""#), [StringValue]); 607 | } 608 | 609 | #[test] 610 | fn block_string() { 611 | assert_eq!(tok_str(r#""""""""#), [r#""""""""#]); 612 | assert_eq!(tok_typ(r#""""""""#), [BlockString]); 613 | assert_eq!(tok_str(r#""""hello""""#), [r#""""hello""""#]); 614 | assert_eq!(tok_typ(r#""""hello""""#), [BlockString]); 615 | assert_eq!(tok_str(r#""""my "quote" """"#), [r#""""my "quote" """"#]); 616 | assert_eq!(tok_typ(r#""""my "quote" """"#), [BlockString]); 617 | assert_eq!(tok_str(r#""""\"""quote" """"#), [r#""""\"""quote" """"#]); 618 | assert_eq!(tok_typ(r#""""\"""quote" """"#), [BlockString]); 619 | } 620 | } 621 | -------------------------------------------------------------------------------- /tests/queries/directive_args.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node @dir(a: 1, b: "2", c: true, d: false, e: null) 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/directive_args_multiline.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node @dir( 3 | a: 1, 4 | b: "2", 5 | c: true, 6 | d: false, 7 | e: null 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /tests/queries/fragment.graphql: -------------------------------------------------------------------------------- 1 | fragment frag on Friend { 2 | node 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/fragment_spread.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node { 3 | id 4 | ...something 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/queries/inline_fragment.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node { 3 | id 4 | ... on User { 5 | name 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/queries/inline_fragment_dir.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node { 3 | id 4 | ... on User @defer { 5 | name 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/queries/kitchen-sink.graphql: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-present, Facebook, Inc. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | query queryName($foo: ComplexType, $site: Site = MOBILE) { 7 | whoever123is: node(id: [123, 456]) { 8 | id , 9 | ... on User @defer { 10 | field2 { 11 | id , 12 | alias: field1(first:10, after:$foo,) @include(if: $foo) { 13 | id, 14 | ...frag 15 | } 16 | } 17 | } 18 | ... @skip(unless: $foo) { 19 | id 20 | } 21 | ... { 22 | id 23 | } 24 | } 25 | } 26 | 27 | mutation likeStory { 28 | like(story: 123) @defer { 29 | story { 30 | id 31 | } 32 | } 33 | } 34 | 35 | subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) { 36 | storyLikeSubscribe(input: $input) { 37 | story { 38 | likers { 39 | count 40 | } 41 | likeSentence { 42 | text 43 | } 44 | } 45 | } 46 | } 47 | 48 | fragment frag on Friend { 49 | foo(size: $size, bar: $b, obj: {key: "value", block: """ 50 | 51 | block string uses \""" 52 | 53 | """}) 54 | } 55 | 56 | { 57 | unnamed(truthy: true, falsey: false, nullish: null), 58 | query 59 | } 60 | -------------------------------------------------------------------------------- /tests/queries/kitchen-sink_canonical.graphql: -------------------------------------------------------------------------------- 1 | query queryName($foo: ComplexType, $site: Site = MOBILE) { 2 | whoever123is: node(id: [123, 456]) { 3 | id 4 | ... on User @defer { 5 | field2 { 6 | id 7 | alias: field1(first: 10, after: $foo) @include(if: $foo) { 8 | id 9 | ...frag 10 | } 11 | } 12 | } 13 | ... @skip(unless: $foo) { 14 | id 15 | } 16 | ... { 17 | id 18 | } 19 | } 20 | } 21 | 22 | mutation likeStory { 23 | like(story: 123) @defer { 24 | story { 25 | id 26 | } 27 | } 28 | } 29 | 30 | subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) { 31 | storyLikeSubscribe(input: $input) { 32 | story { 33 | likers { 34 | count 35 | } 36 | likeSentence { 37 | text 38 | } 39 | } 40 | } 41 | } 42 | 43 | fragment frag on Friend { 44 | foo(size: $size, bar: $b, obj: {block: "block string uses \"\"\"", key: "value"}) 45 | } 46 | 47 | { 48 | unnamed(truthy: true, falsey: false, nullish: null) 49 | query 50 | } 51 | -------------------------------------------------------------------------------- /tests/queries/minimal.graphql: -------------------------------------------------------------------------------- 1 | { 2 | a 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/minimal_mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation { 2 | notify 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/minimal_query.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/mutation_directive.graphql: -------------------------------------------------------------------------------- 1 | mutation @directive { 2 | node 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/mutation_nameless_vars.graphql: -------------------------------------------------------------------------------- 1 | mutation($first: Int, $second: Int) { 2 | field1(first: $first) 3 | field2(second: $second) 4 | } 5 | -------------------------------------------------------------------------------- /tests/queries/named_query.graphql: -------------------------------------------------------------------------------- 1 | query Foo { 2 | field 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/nested_selection.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/queries/query_aliases.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | an_alias: node 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_arguments.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node(id: 1) 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_arguments_multiline.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node( 3 | id: 1 4 | ) 5 | node( 6 | id: 1, 7 | one: 3 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /tests/queries/query_array_argument_multiline.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node( 3 | id: [ 4 | 5, 5 | 6, 6 | 7 7 | ] 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /tests/queries/query_directive.graphql: -------------------------------------------------------------------------------- 1 | query @directive { 2 | node 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_list_argument.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node(id: 1, list: [123, 456]) 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_nameless_vars.graphql: -------------------------------------------------------------------------------- 1 | query($first: Int, $second: Int) { 2 | field1(first: $first) 3 | field2(second: $second) 4 | } 5 | -------------------------------------------------------------------------------- /tests/queries/query_nameless_vars_multiple_fields.graphql: -------------------------------------------------------------------------------- 1 | ,,,,,,,,,,,,,,,,, 2 | query ,,,,,,, ($houseId: String!, $streetNumber: Int!) ,,,,,,,,,,,, { # comment 3 | ,,,,,,,,,,,,,,,,,, # commas should be fine 4 | house(id: $houseId) { 5 | id 6 | name 7 | lat 8 | lng 9 | } 10 | street(number: $streetNumber) { # this is a comment 11 | id 12 | } 13 | houseStreet(id: $houseId, number: $streetNumber) { 14 | id 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/queries/query_nameless_vars_multiple_fields_canonical.graphql: -------------------------------------------------------------------------------- 1 | query($houseId: String!, $streetNumber: Int!) { 2 | house(id: $houseId) { 3 | id 4 | name 5 | lat 6 | lng 7 | } 8 | street(number: $streetNumber) { 9 | id 10 | } 11 | houseStreet(id: $houseId, number: $streetNumber) { 12 | id 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/queries/query_object_argument.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node(id: 1, obj: {key1: 123, key2: 456}) 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_object_argument_multiline.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node( 3 | id: 1, 4 | obj: { 5 | key1: 123, 6 | key2: 456 7 | } 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /tests/queries/query_var_default_float.graphql: -------------------------------------------------------------------------------- 1 | query Foo($site: Float = 0.5) { 2 | field 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_var_default_list.graphql: -------------------------------------------------------------------------------- 1 | query Foo($site: [Int] = [123, 456]) { 2 | field 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_var_default_object.graphql: -------------------------------------------------------------------------------- 1 | query Foo($site: Site = {url: null}) { 2 | field 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_var_default_string.graphql: -------------------------------------------------------------------------------- 1 | query Foo($site: String = "string") { 2 | field 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_var_defaults.graphql: -------------------------------------------------------------------------------- 1 | query Foo($site: Site = MOBILE) { 2 | field 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/query_vars.graphql: -------------------------------------------------------------------------------- 1 | query Foo($arg: SomeType) { 2 | field 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/string_literal.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node(id: "hello") 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/subscription_directive.graphql: -------------------------------------------------------------------------------- 1 | subscription @directive { 2 | node 3 | } 4 | -------------------------------------------------------------------------------- /tests/queries/triple_quoted_literal.graphql: -------------------------------------------------------------------------------- 1 | query { 2 | node(id: """ 3 | Hello, 4 | world! 5 | """) 6 | } 7 | -------------------------------------------------------------------------------- /tests/query_errors.rs: -------------------------------------------------------------------------------- 1 | extern crate graphql_parser; 2 | #[cfg(test)] 3 | #[macro_use] 4 | extern crate pretty_assertions; 5 | 6 | use std::fs::File; 7 | use std::io::Read; 8 | 9 | use graphql_parser::parse_query; 10 | 11 | fn test_error(filename: &str) { 12 | let mut buf = String::with_capacity(1024); 13 | let path = format!("tests/query_errors/{}.txt", filename); 14 | let mut f = File::open(path).unwrap(); 15 | f.read_to_string(&mut buf).unwrap(); 16 | let mut iter = buf.splitn(2, "\n---\n"); 17 | let graphql = iter.next().unwrap(); 18 | let expected = iter.next().expect("file should contain error message"); 19 | let err = parse_query::(graphql).unwrap_err(); 20 | assert_eq!(err.to_string(), expected); 21 | } 22 | 23 | #[test] 24 | fn invalid_curly_brace() { 25 | test_error("invalid_curly_brace"); 26 | } 27 | #[test] 28 | fn bad_args() { 29 | test_error("bad_args"); 30 | } 31 | -------------------------------------------------------------------------------- /tests/query_errors/bad_args.txt: -------------------------------------------------------------------------------- 1 | query MyQuery { 2 | field1([something]) 3 | } 4 | --- 5 | query parse error: Parse error at 2:10 6 | Unexpected `[[Punctuator]` 7 | Expected Name 8 | -------------------------------------------------------------------------------- /tests/query_errors/invalid_curly_brace.txt: -------------------------------------------------------------------------------- 1 | querry MyQuery { 2 | field1 3 | } 4 | --- 5 | query parse error: Parse error at 1:1 6 | Unexpected `querry[Name]` 7 | Expected {, query, mutation, subscription or fragment 8 | -------------------------------------------------------------------------------- /tests/query_roundtrips.rs: -------------------------------------------------------------------------------- 1 | extern crate graphql_parser; 2 | #[cfg(test)] 3 | #[macro_use] 4 | extern crate pretty_assertions; 5 | 6 | use std::fs::File; 7 | use std::io::Read; 8 | 9 | use graphql_parser::{parse_query, Style}; 10 | 11 | fn roundtrip_multiline_args(filename: &str) { 12 | roundtrip(filename, Style::default().multiline_arguments(true)) 13 | } 14 | 15 | fn roundtrip_default(filename: &str) { 16 | roundtrip(filename, &Style::default()) 17 | } 18 | 19 | fn roundtrip(filename: &str, style: &Style) { 20 | let mut buf = String::with_capacity(1024); 21 | let path = format!("tests/queries/{}.graphql", filename); 22 | let mut f = File::open(path).unwrap(); 23 | f.read_to_string(&mut buf).unwrap(); 24 | let ast = parse_query::(&buf).unwrap().to_owned(); 25 | assert_eq!(ast.format(style), buf); 26 | } 27 | 28 | fn roundtrip2(filename: &str) { 29 | let mut buf = String::with_capacity(1024); 30 | let source = format!("tests/queries/{}.graphql", filename); 31 | let target = format!("tests/queries/{}_canonical.graphql", filename); 32 | let mut f = File::open(source).unwrap(); 33 | f.read_to_string(&mut buf).unwrap(); 34 | let ast = parse_query::(&buf).unwrap().to_owned(); 35 | 36 | let mut buf = String::with_capacity(1024); 37 | let mut f = File::open(target).unwrap(); 38 | f.read_to_string(&mut buf).unwrap(); 39 | assert_eq!(ast.to_string(), buf); 40 | } 41 | 42 | #[test] 43 | fn minimal() { 44 | roundtrip_default("minimal"); 45 | } 46 | #[test] 47 | fn minimal_query() { 48 | roundtrip_default("minimal_query"); 49 | } 50 | #[test] 51 | fn named_query() { 52 | roundtrip_default("named_query"); 53 | } 54 | #[test] 55 | fn query_vars() { 56 | roundtrip_default("query_vars"); 57 | } 58 | #[test] 59 | fn query_nameless_vars() { 60 | roundtrip_default("query_nameless_vars"); 61 | } 62 | #[test] 63 | fn query_nameless_vars_multiple_fields() { 64 | roundtrip2("query_nameless_vars_multiple_fields"); 65 | } 66 | #[test] 67 | fn query_var_defaults() { 68 | roundtrip_default("query_var_defaults"); 69 | } 70 | #[test] 71 | fn query_var_defaults1() { 72 | roundtrip_default("query_var_default_string"); 73 | } 74 | #[test] 75 | fn query_var_defaults2() { 76 | roundtrip_default("query_var_default_float"); 77 | } 78 | #[test] 79 | fn query_var_defaults3() { 80 | roundtrip_default("query_var_default_list"); 81 | } 82 | #[test] 83 | fn query_var_defaults4() { 84 | roundtrip_default("query_var_default_object"); 85 | } 86 | #[test] 87 | fn query_aliases() { 88 | roundtrip_default("query_aliases"); 89 | } 90 | #[test] 91 | fn query_arguments() { 92 | roundtrip_default("query_arguments"); 93 | } 94 | #[test] 95 | fn query_arguments_multiline() { 96 | roundtrip_multiline_args("query_arguments_multiline"); 97 | } 98 | #[test] 99 | fn query_directive() { 100 | roundtrip_default("query_directive"); 101 | } 102 | #[test] 103 | fn mutation_directive() { 104 | roundtrip_default("mutation_directive"); 105 | } 106 | #[test] 107 | fn mutation_nameless_vars() { 108 | roundtrip_default("mutation_nameless_vars"); 109 | } 110 | #[test] 111 | fn subscription_directive() { 112 | roundtrip_default("subscription_directive"); 113 | } 114 | #[test] 115 | fn string_literal() { 116 | roundtrip_default("string_literal"); 117 | } 118 | #[test] 119 | fn triple_quoted_literal() { 120 | roundtrip_default("triple_quoted_literal"); 121 | } 122 | #[test] 123 | fn query_list_arg() { 124 | roundtrip_default("query_list_argument"); 125 | } 126 | #[test] 127 | fn query_object_arg() { 128 | roundtrip_default("query_object_argument"); 129 | } 130 | #[test] 131 | fn query_object_arg_multiline() { 132 | roundtrip_multiline_args("query_object_argument_multiline"); 133 | } 134 | #[test] 135 | fn query_array_arg_multiline() { 136 | roundtrip_multiline_args("query_array_argument_multiline"); 137 | } 138 | #[test] 139 | fn nested_selection() { 140 | roundtrip_default("nested_selection"); 141 | } 142 | #[test] 143 | fn inline_fragment() { 144 | roundtrip_default("inline_fragment"); 145 | } 146 | #[test] 147 | fn inline_fragment_dir() { 148 | roundtrip_default("inline_fragment_dir"); 149 | } 150 | #[test] 151 | fn fragment_spread() { 152 | roundtrip_default("fragment_spread"); 153 | } 154 | #[test] 155 | fn minimal_mutation() { 156 | roundtrip_default("minimal_mutation"); 157 | } 158 | #[test] 159 | fn fragment() { 160 | roundtrip_default("fragment"); 161 | } 162 | #[test] 163 | fn directive_args() { 164 | roundtrip_default("directive_args"); 165 | } 166 | #[test] 167 | fn directive_args_multiline() { 168 | roundtrip_multiline_args("directive_args_multiline"); 169 | } 170 | #[test] 171 | fn kitchen_sink() { 172 | roundtrip2("kitchen-sink"); 173 | } 174 | -------------------------------------------------------------------------------- /tests/schema_roundtrips.rs: -------------------------------------------------------------------------------- 1 | extern crate graphql_parser; 2 | #[cfg(test)] 3 | #[macro_use] 4 | extern crate pretty_assertions; 5 | 6 | use std::fs::File; 7 | use std::io::Read; 8 | 9 | use graphql_parser::parse_schema; 10 | 11 | fn roundtrip(filename: &str) { 12 | let mut buf = String::with_capacity(1024); 13 | let path = format!("tests/schemas/{}.graphql", filename); 14 | let mut f = File::open(path).unwrap(); 15 | f.read_to_string(&mut buf).unwrap(); 16 | let ast = parse_schema::(&buf).unwrap().to_owned(); 17 | assert_eq!(ast.to_string(), buf); 18 | } 19 | 20 | fn roundtrip2(filename: &str) { 21 | let mut buf = String::with_capacity(1024); 22 | let source = format!("tests/schemas/{}.graphql", filename); 23 | let target = format!("tests/schemas/{}_canonical.graphql", filename); 24 | let mut f = File::open(source).unwrap(); 25 | f.read_to_string(&mut buf).unwrap(); 26 | let ast = parse_schema::(&buf).unwrap(); 27 | 28 | let mut buf = String::with_capacity(1024); 29 | let mut f = File::open(target).unwrap(); 30 | f.read_to_string(&mut buf).unwrap(); 31 | assert_eq!(ast.to_string(), buf); 32 | } 33 | 34 | #[test] 35 | fn minimal() { 36 | roundtrip("minimal"); 37 | } 38 | #[test] 39 | fn scalar_type() { 40 | roundtrip("scalar_type"); 41 | } 42 | #[test] 43 | fn extend_scalar() { 44 | roundtrip("extend_scalar"); 45 | } 46 | #[test] 47 | fn minimal_type() { 48 | roundtrip("minimal_type"); 49 | } 50 | #[test] 51 | fn implements() { 52 | roundtrip("implements"); 53 | } 54 | #[test] 55 | fn implements_amp() { 56 | roundtrip2("implements_amp"); 57 | } 58 | #[test] 59 | fn implements_interface() { 60 | roundtrip("implements_interface"); 61 | } 62 | #[test] 63 | fn simple_object() { 64 | roundtrip("simple_object"); 65 | } 66 | #[test] 67 | fn extend_object() { 68 | roundtrip("extend_object"); 69 | } 70 | #[test] 71 | fn interface() { 72 | roundtrip("interface"); 73 | } 74 | #[test] 75 | fn extend_interface() { 76 | roundtrip("extend_interface"); 77 | } 78 | #[test] 79 | fn union() { 80 | roundtrip("union"); 81 | } 82 | #[test] 83 | fn empty_union() { 84 | roundtrip("empty_union"); 85 | } 86 | #[test] 87 | fn union_extension() { 88 | roundtrip("union_extension"); 89 | } 90 | #[test] 91 | fn enum_type() { 92 | roundtrip("enum"); 93 | } 94 | #[test] 95 | fn extend_enum() { 96 | roundtrip("extend_enum"); 97 | } 98 | #[test] 99 | fn input_type() { 100 | roundtrip("input_type"); 101 | } 102 | #[test] 103 | fn extend_input() { 104 | roundtrip2("extend_input"); 105 | } 106 | #[test] 107 | fn directive() { 108 | roundtrip("directive"); 109 | } 110 | #[test] 111 | fn kitchen_sink() { 112 | roundtrip2("kitchen-sink"); 113 | } 114 | #[test] 115 | fn directive_descriptions() { 116 | roundtrip2("directive_descriptions"); 117 | } 118 | #[test] 119 | fn directive_variable_definition() { 120 | roundtrip("directive_variable_definition"); 121 | } 122 | #[test] 123 | fn repeatable() { 124 | roundtrip("repeatable") 125 | } 126 | -------------------------------------------------------------------------------- /tests/schemas/directive.graphql: -------------------------------------------------------------------------------- 1 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 2 | -------------------------------------------------------------------------------- /tests/schemas/directive_descriptions.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | Directs the executor to include this field or 3 | fragment only when the `if` argument is true. 4 | """ 5 | directive @include( 6 | """ 7 | Included when true. 8 | """ 9 | if: Boolean! 10 | ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 11 | 12 | """ 13 | Directs the executor to skip this field or 14 | fragment when the `if` argument is true. 15 | """ 16 | directive @skip( 17 | """ 18 | Skipped when true. 19 | """ 20 | if: Boolean! 21 | ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 22 | -------------------------------------------------------------------------------- /tests/schemas/directive_descriptions_canonical.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | Directs the executor to include this field or 3 | fragment only when the `if` argument is true. 4 | """ 5 | directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 6 | 7 | """ 8 | Directs the executor to skip this field or 9 | fragment when the `if` argument is true. 10 | """ 11 | directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 12 | -------------------------------------------------------------------------------- /tests/schemas/directive_variable_definition.graphql: -------------------------------------------------------------------------------- 1 | directive @configurable on VARIABLE_DEFINITION 2 | -------------------------------------------------------------------------------- /tests/schemas/empty_union.graphql: -------------------------------------------------------------------------------- 1 | union UndefinedUnion 2 | -------------------------------------------------------------------------------- /tests/schemas/enum.graphql: -------------------------------------------------------------------------------- 1 | enum Site { 2 | DESKTOP 3 | MOBILE 4 | } 5 | -------------------------------------------------------------------------------- /tests/schemas/extend_enum.graphql: -------------------------------------------------------------------------------- 1 | extend enum Site { 2 | VR 3 | } 4 | -------------------------------------------------------------------------------- /tests/schemas/extend_input.graphql: -------------------------------------------------------------------------------- 1 | extend input InputType { 2 | other: Float = 1.23e4 3 | } 4 | -------------------------------------------------------------------------------- /tests/schemas/extend_input_canonical.graphql: -------------------------------------------------------------------------------- 1 | extend input InputType { 2 | other: Float = 12300 3 | } 4 | -------------------------------------------------------------------------------- /tests/schemas/extend_interface.graphql: -------------------------------------------------------------------------------- 1 | extend interface Bar { 2 | two(argument: InputType!): Type 3 | } 4 | 5 | extend interface Foo implements IOne & ITwo { 6 | three(argument: [InputType!]!): Type 7 | } 8 | -------------------------------------------------------------------------------- /tests/schemas/extend_object.graphql: -------------------------------------------------------------------------------- 1 | extend type Foo { 2 | seven(argument: [String]): Type 3 | } 4 | 5 | extend type Bar implements IOne & ITwo { 6 | five(argument: [String!]!): Type 7 | } 8 | -------------------------------------------------------------------------------- /tests/schemas/extend_scalar.graphql: -------------------------------------------------------------------------------- 1 | extend scalar CustomScalar @onScalar 2 | -------------------------------------------------------------------------------- /tests/schemas/implements.graphql: -------------------------------------------------------------------------------- 1 | type Type1 implements IOne 2 | 3 | type Type1 implements IOne & ITwo 4 | -------------------------------------------------------------------------------- /tests/schemas/implements_amp.graphql: -------------------------------------------------------------------------------- 1 | type Type1 implements & IOne & ITwo 2 | type Type2 implements & IOne 3 | -------------------------------------------------------------------------------- /tests/schemas/implements_amp_canonical.graphql: -------------------------------------------------------------------------------- 1 | type Type1 implements IOne & ITwo 2 | 3 | type Type2 implements IOne 4 | -------------------------------------------------------------------------------- /tests/schemas/implements_interface.graphql: -------------------------------------------------------------------------------- 1 | interface IOne implements ITwo 2 | 3 | interface IThree implements IFour & IFive 4 | -------------------------------------------------------------------------------- /tests/schemas/input_type.graphql: -------------------------------------------------------------------------------- 1 | input InputType { 2 | key: String! 3 | answer: Int = 42 4 | } 5 | -------------------------------------------------------------------------------- /tests/schemas/interface.graphql: -------------------------------------------------------------------------------- 1 | interface Bar { 2 | one: Type 3 | } 4 | -------------------------------------------------------------------------------- /tests/schemas/kitchen-sink.graphql: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015-present, Facebook, Inc. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | schema { 7 | query: QueryType 8 | mutation: MutationType 9 | } 10 | 11 | """ 12 | This is a description 13 | of the `Foo` type. 14 | """ 15 | type Foo implements Bar & Baz { 16 | one: Type 17 | two(argument: InputType!): Type 18 | three(argument: InputType, other: String): Int 19 | four(argument: String = "string"): String 20 | five(argument: [String] = ["string", "string"]): String 21 | six(argument: InputType = {key: "value"}): Type 22 | seven(argument: Int = null): Type 23 | } 24 | 25 | type AnnotatedObject @onObject(arg: "value") { 26 | annotatedField(arg: Type = "default" @onArg): Type @onField 27 | } 28 | 29 | type UndefinedType 30 | 31 | extend type Foo { 32 | seven(argument: [String]): Type 33 | } 34 | 35 | extend type Foo @onType 36 | 37 | interface Bar { 38 | one: Type 39 | four(argument: String = "string"): String 40 | } 41 | 42 | interface AnnotatedInterface @onInterface { 43 | annotatedField(arg: Type @onArg): Type @onField 44 | } 45 | 46 | interface UndefinedInterface 47 | 48 | extend interface Bar { 49 | two(argument: InputType!): Type 50 | } 51 | 52 | extend interface Bar @onInterface 53 | 54 | union Feed = Story | Article | Advert 55 | 56 | union AnnotatedUnion @onUnion = A | B 57 | 58 | union AnnotatedUnionTwo @onUnion = | A | B 59 | 60 | union UndefinedUnion 61 | 62 | extend union Feed = Photo | Video 63 | 64 | extend union Feed @onUnion 65 | 66 | scalar CustomScalar 67 | 68 | scalar AnnotatedScalar @onScalar 69 | 70 | extend scalar CustomScalar @onScalar 71 | 72 | enum Site { 73 | DESKTOP 74 | MOBILE 75 | } 76 | 77 | enum AnnotatedEnum @onEnum { 78 | ANNOTATED_VALUE @onEnumValue 79 | OTHER_VALUE 80 | } 81 | 82 | enum UndefinedEnum 83 | 84 | extend enum Site { 85 | VR 86 | } 87 | 88 | extend enum Site @onEnum 89 | 90 | input InputType { 91 | key: String! 92 | answer: Int = 42 93 | } 94 | 95 | input AnnotatedInput @onInputObject { 96 | annotatedField: Type @onField 97 | } 98 | 99 | input UndefinedInput 100 | 101 | extend input InputType { 102 | other: Float = 1.23e4 103 | } 104 | 105 | extend input InputType @onInputObject 106 | 107 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 108 | 109 | directive @include(if: Boolean!) 110 | on FIELD 111 | | FRAGMENT_SPREAD 112 | | INLINE_FRAGMENT 113 | 114 | directive @include2(if: Boolean!) on 115 | | FIELD 116 | | FRAGMENT_SPREAD 117 | | INLINE_FRAGMENT 118 | -------------------------------------------------------------------------------- /tests/schemas/kitchen-sink_canonical.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryType 3 | mutation: MutationType 4 | } 5 | 6 | """ 7 | This is a description 8 | of the `Foo` type. 9 | """ 10 | type Foo implements Bar & Baz { 11 | one: Type 12 | two(argument: InputType!): Type 13 | three(argument: InputType, other: String): Int 14 | four(argument: String = "string"): String 15 | five(argument: [String] = ["string", "string"]): String 16 | six(argument: InputType = {key: "value"}): Type 17 | seven(argument: Int = null): Type 18 | } 19 | 20 | type AnnotatedObject @onObject(arg: "value") { 21 | annotatedField(arg: Type = "default" @onArg): Type @onField 22 | } 23 | 24 | type UndefinedType 25 | 26 | extend type Foo { 27 | seven(argument: [String]): Type 28 | } 29 | 30 | extend type Foo @onType 31 | 32 | interface Bar { 33 | one: Type 34 | four(argument: String = "string"): String 35 | } 36 | 37 | interface AnnotatedInterface @onInterface { 38 | annotatedField(arg: Type @onArg): Type @onField 39 | } 40 | 41 | interface UndefinedInterface 42 | 43 | extend interface Bar { 44 | two(argument: InputType!): Type 45 | } 46 | 47 | extend interface Bar @onInterface 48 | 49 | union Feed = Story | Article | Advert 50 | 51 | union AnnotatedUnion @onUnion = A | B 52 | 53 | union AnnotatedUnionTwo @onUnion = A | B 54 | 55 | union UndefinedUnion 56 | 57 | extend union Feed = Photo | Video 58 | 59 | extend union Feed @onUnion 60 | 61 | scalar CustomScalar 62 | 63 | scalar AnnotatedScalar @onScalar 64 | 65 | extend scalar CustomScalar @onScalar 66 | 67 | enum Site { 68 | DESKTOP 69 | MOBILE 70 | } 71 | 72 | enum AnnotatedEnum @onEnum { 73 | ANNOTATED_VALUE @onEnumValue 74 | OTHER_VALUE 75 | } 76 | 77 | enum UndefinedEnum 78 | 79 | extend enum Site { 80 | VR 81 | } 82 | 83 | extend enum Site @onEnum 84 | 85 | input InputType { 86 | key: String! 87 | answer: Int = 42 88 | } 89 | 90 | input AnnotatedInput @onInputObject { 91 | annotatedField: Type @onField 92 | } 93 | 94 | input UndefinedInput 95 | 96 | extend input InputType { 97 | other: Float = 12300 98 | } 99 | 100 | extend input InputType @onInputObject 101 | 102 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 103 | 104 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 105 | 106 | directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 107 | -------------------------------------------------------------------------------- /tests/schemas/minimal.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | -------------------------------------------------------------------------------- /tests/schemas/minimal_type.graphql: -------------------------------------------------------------------------------- 1 | type UndefinedType 2 | -------------------------------------------------------------------------------- /tests/schemas/repeatable.graphql: -------------------------------------------------------------------------------- 1 | directive @filter(expression: String!) repeatable on FIELD 2 | -------------------------------------------------------------------------------- /tests/schemas/scalar_type.graphql: -------------------------------------------------------------------------------- 1 | "This is the best scalar type" 2 | scalar BestType @perfectness(value: 100500) 3 | -------------------------------------------------------------------------------- /tests/schemas/simple_object.graphql: -------------------------------------------------------------------------------- 1 | type Foo { 2 | bar: Type 3 | } 4 | -------------------------------------------------------------------------------- /tests/schemas/union.graphql: -------------------------------------------------------------------------------- 1 | union Feed = Story | Article | Advert 2 | -------------------------------------------------------------------------------- /tests/schemas/union_extension.graphql: -------------------------------------------------------------------------------- 1 | extend union Feed = Photo | Video 2 | -------------------------------------------------------------------------------- /vagga.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | 3 | make: !Command 4 | description: Build the library 5 | container: ubuntu 6 | run: [cargo, build] 7 | 8 | make-wasm: !Command 9 | description: Build wasm library (just to check it's buildable) 10 | container: ubuntu 11 | run: [cargo, build, --target=wasm32-unknown-unknown] 12 | 13 | cargo: !Command 14 | description: Run arbitrary cargo command 15 | symlink-name: cargo 16 | container: ubuntu 17 | run: [cargo] 18 | 19 | test: !Command 20 | description: Run tests 21 | container: ubuntu 22 | run: [cargo, test] 23 | 24 | _bulk: !Command 25 | description: Run `bulk` command (for version bookkeeping) 26 | container: ubuntu 27 | run: [bulk] 28 | 29 | containers: 30 | 31 | ubuntu: 32 | setup: 33 | - !Ubuntu xenial 34 | - !Install [ca-certificates, git, build-essential, vim] 35 | 36 | - !TarInstall 37 | url: "https://static.rust-lang.org/dist/rust-1.42.0-x86_64-unknown-linux-gnu.tar.gz" 38 | script: "./install.sh --prefix=/usr \ 39 | --components=rustc,rust-std-x86_64-unknown-linux-gnu,cargo" 40 | - !TarInstall 41 | url: "https://static.rust-lang.org/dist/rust-std-1.42.0-wasm32-unknown-unknown.tar.gz" 42 | script: "./install.sh --prefix=/usr --components=rust-std-wasm32-unknown-unknown" 43 | - &bulk !Tar 44 | url: "https://github.com/tailhook/bulk/releases/download/v0.4.10/bulk-v0.4.10.tar.gz" 45 | sha256: 481513f8a0306a9857d045497fb5b50b50a51e9ff748909ecf7d2bda1de275ab 46 | path: / 47 | - !Sh | 48 | cargo install cargo-fix --root=/usr 49 | 50 | 51 | environ: 52 | HOME: /work/target 53 | RUST_BACKTRACE: 1 54 | --------------------------------------------------------------------------------