├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── benchmark ├── compare_benchmarks.cr └── lib │ ├── libragphqlparser.cr │ └── libragphqlparserC.cr ├── docs ├── CLTK.html ├── CLTK │ ├── TokenValue.html │ └── Type.html ├── GraphQL.html ├── GraphQL │ ├── Directive.html │ ├── Directives.html │ ├── Directives │ │ ├── IncludeDirective.html │ │ ├── IsDeprecated.html │ │ └── SkipDirective.html │ ├── Error.html │ ├── Language.html │ ├── Language │ │ ├── AEnum.html │ │ ├── ASTNode.html │ │ ├── AbstractNode.html │ │ ├── Argument.html │ │ ├── ArgumentValue.html │ │ ├── Directive.html │ │ ├── DirectiveDefinition.html │ │ ├── Document.html │ │ ├── EnumTypeDefinition.html │ │ ├── EnumValueDefinition.html │ │ ├── FValue.html │ │ ├── Field.html │ │ ├── FieldDefinition.html │ │ ├── FragmentDefinition.html │ │ ├── FragmentSpread.html │ │ ├── Generation.html │ │ ├── InlineFragment.html │ │ ├── InputObject.html │ │ ├── InputObjectTypeDefinition.html │ │ ├── InputValueDefinition.html │ │ ├── InterfaceTypeDefinition.html │ │ ├── Lexer.html │ │ ├── LexerContext.html │ │ ├── ListType.html │ │ ├── NameOnlyNode.html │ │ ├── NonNullType.html │ │ ├── NullValue.html │ │ ├── ObjectTypeDefinition.html │ │ ├── OperationDefinition.html │ │ ├── Parser.html │ │ ├── ParserContext.html │ │ ├── ScalarTypeDefinition.html │ │ ├── SchemaDefinition.html │ │ ├── Selection.html │ │ ├── Token.html │ │ ├── Token │ │ │ └── Kind.html │ │ ├── Type.html │ │ ├── TypeDefinition.html │ │ ├── TypeName.html │ │ ├── UnionTypeDefinition.html │ │ ├── VariableDefinition.html │ │ ├── VariableIdentifier.html │ │ └── WrapperType.html │ ├── ObjectType.html │ ├── Schema.html │ ├── Schema │ │ ├── AlibiType.html │ │ ├── Context.html │ │ ├── FragmentResolver.html │ │ ├── InputType.html │ │ ├── Introspection.html │ │ ├── Introspection │ │ │ └── IntrospectionObject.html │ │ ├── Middleware.html │ │ ├── Middleware │ │ │ └── Proc.html │ │ ├── Schema.html │ │ ├── Schema │ │ │ ├── Args.html │ │ │ ├── ExecuteParams.html │ │ │ └── JSONType.html │ │ └── VariableResolver.html │ └── TypeValidation.html ├── NamedTuple.html ├── Object.html ├── css │ └── style.css ├── index.html ├── index.json ├── js │ └── doc.js ├── search-index.js └── toplevel.html ├── example ├── simple_blog_example.cr └── simple_example.cr ├── shard.yml ├── show_performance.sh ├── spec ├── graphql-crystal │ ├── directive_spec.cr │ ├── introspection │ │ ├── directive_type_spec.cr │ │ ├── input_value_type_spec.cr │ │ ├── introspection_query_spec.cr │ │ ├── schema_type_spec.cr │ │ └── type_type_spec.cr │ ├── language │ │ ├── generation_spec.cr │ │ ├── lexer_spec.cr │ │ └── parser_spec.cr │ ├── schema │ │ ├── custom_context_spec.cr │ │ └── variables_spec.cr │ ├── schema_spec.cr │ ├── support │ │ ├── go_graphql_test_schema_spec.cr │ │ ├── star_wars_schema_introspection_spec.cr │ │ └── star_wars_schema_spec.cr │ └── validation │ │ ├── evaluation_depth.cr │ │ └── type_validation.cr ├── graphql-crystal_spec.cr ├── spec_helper.cr └── support │ ├── custom_context_schema.cr │ ├── dummy │ ├── dummy_data.cr │ ├── dummy_schema.cr │ └── dummy_schema_string.cr │ ├── go_graphql_test_schema.cr │ ├── star_wars │ ├── star_wars_data.cr │ └── star_wars_schema.cr │ └── test_schema.cr └── src ├── core_ext ├── named_tuple.cr └── object.cr ├── graphql-crystal.cr └── graphql-crystal ├── directives.cr ├── directives ├── deprecated_directive.cr ├── directive.cr ├── include_directive.cr └── skip_directive.cr ├── language.cr ├── language ├── ast.cr ├── generation.cr ├── lexer.cr ├── lexer_context.cr ├── nodes.cr ├── parser.cr ├── parser_context.cr ├── token.cr └── type.cr ├── schema.cr ├── schema ├── fragment_resolver.cr ├── introspection_query.cr ├── middleware.cr ├── schema.cr ├── schema_execute.cr ├── schema_introspection.cr └── variable_resolver.cr ├── types ├── object_type.cr └── type_validation.cr └── version.cr /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | 6 | # Libraries don't need dependency lock 7 | # Dependencies will be locked in application that uses them 8 | /shard.lock 9 | /parser.bin 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | script: 3 | - crystal spec 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.1.3 2 | - add `Schema#add_input_type` to enable automatic parsing of query variables to crystal structs 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-crystal [![Build Status](https://api.travis-ci.org/ziprandom/graphql-crystal.svg)](https://travis-ci.org/ziprandom/graphql-crystal) 2 | 3 | 4 | An implementation of [GraphQL](http://graphql.org/learn/) for the crystal programming language inspired by [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) & [go-graphql](https://github.com/playlyfe/go-graphql) & [graphql-parser](https://github.com/graphql-dotnet/parser). 5 | 6 | The library is in beta state atm. Should already be usable but expect to find bugs (and open issues about them). pull-requests, suggestions & criticism are very welcome! 7 | 8 | Find the api docs [here](https://ziprandom.github.io/graphql-crystal/). 9 | 10 | ## Installation 11 | 12 | Add this to your application's `shard.yml`: 13 | 14 | ```yaml 15 | dependencies: 16 | graphql-crystal: 17 | github: ziprandom/graphql-crystal 18 | ``` 19 | 20 | ## Usage 21 | 22 | Complete source [here](example/simple_example.cr). 23 | 24 | Given this simple domain model of users and posts 25 | 26 | ```cr 27 | class User 28 | property name 29 | def initialize(@name : String); end 30 | end 31 | 32 | class Post 33 | property :title, :body, :author 34 | def initialize(@title : String, @body : String, @author : User); end 35 | end 36 | 37 | POSTS = [] of Post 38 | USERS = [User.new("Alice"), User.new("Bob")] 39 | ``` 40 | 41 | We can instantiate a GraphQL schema directly from a graphql schema definition string 42 | 43 | ```cr 44 | schema = GraphQL::Schema.from_schema( 45 | %{ 46 | schema { 47 | query: QueryType, 48 | mutation: MutationType 49 | } 50 | 51 | type QueryType { 52 | posts: [PostType] 53 | users: [UserType] 54 | user(name: String!): UserType 55 | } 56 | 57 | type MutationType { 58 | post(post: PostInput) : PostType 59 | } 60 | 61 | input PostInput { 62 | author: String! 63 | title: String! 64 | body: String! 65 | } 66 | 67 | type UserType { 68 | name: String 69 | posts: [PostType] 70 | } 71 | 72 | type PostType { 73 | author: UserType 74 | title: String 75 | body: String 76 | } 77 | } 78 | ) 79 | ``` 80 | 81 | Then we create the backing types by including the ```GraphQL::ObjectType``` and defining the fields using the ```field``` macro 82 | 83 | ```cr 84 | # reopening User and Post class 85 | class User 86 | include GraphQL::ObjectType 87 | 88 | # defaults to the method of 89 | # the same name without block 90 | field :name 91 | 92 | field :posts do 93 | POSTS.select &.author.==(self) 94 | end 95 | end 96 | 97 | class Post 98 | include GraphQL::ObjectType 99 | field :title 100 | field :body 101 | field :author 102 | end 103 | ``` 104 | 105 | Now we define the top level queries 106 | 107 | ```cr 108 | # extend self when using a module or a class (not an instance) 109 | # as the actual Object 110 | 111 | module QueryType 112 | include GraphQL::ObjectType 113 | extend self 114 | 115 | field :users do 116 | USERS 117 | end 118 | 119 | field :user do |args| 120 | USERS.find( &.name.==(args["name"].as(String)) ) || raise "no user by that name" 121 | end 122 | 123 | field :posts do 124 | POSTS 125 | end 126 | end 127 | 128 | module MutationType 129 | include GraphQL::ObjectType 130 | extend self 131 | 132 | field :post do |args| 133 | 134 | user = USERS.find &.name.==( 135 | args["post"].as(Hash)["author"].as(String) 136 | ) 137 | raise "author doesn't exist" unless user 138 | 139 | ( 140 | POSTS << Post.new( 141 | args["post"].as(Hash)["title"].as(String), 142 | args["post"].as(Hash)["body"].as(String), 143 | user 144 | ) 145 | ).last 146 | end 147 | end 148 | ``` 149 | 150 | Finally set the top level Object Types on the schema 151 | 152 | ```cr 153 | schema.query_resolver = QueryType 154 | schema.mutation_resolver = MutationType 155 | ``` 156 | 157 | And we are ready to run some tests 158 | 159 | ```cr 160 | describe "my graphql schema" do 161 | it "does queries" do 162 | schema.execute("{ users { name posts } }") 163 | .should eq ({ 164 | "data" => { 165 | "users" => [ 166 | { 167 | "name" => "Alice", 168 | "posts" => [] of String 169 | }, 170 | { 171 | "name" => "Bob", 172 | "posts" => [] of String 173 | } 174 | ] 175 | } 176 | }) 177 | end 178 | 179 | it "does mutations" do 180 | 181 | mutation_string = %{ 182 | mutation post($post: PostInput) { 183 | post(post: $post) { 184 | author { 185 | name 186 | posts { title } 187 | } 188 | title 189 | body 190 | } 191 | } 192 | } 193 | 194 | payload = { 195 | "post" => { 196 | "author" => "Alice", 197 | "title" => "the long and windy road", 198 | "body" => "that leads to your door" 199 | } 200 | } 201 | 202 | schema.execute(mutation_string, payload) 203 | .should eq ({ 204 | "data" => { 205 | "post" => { 206 | "title" => "the long and windy road", 207 | "body" => "that leads to your door", 208 | "author" => { 209 | "name" => "Alice", 210 | "posts" => [ 211 | { 212 | "title" => "the long and windy road" 213 | } 214 | ] 215 | } 216 | } 217 | } 218 | }) 219 | end 220 | end 221 | ``` 222 | 223 | ### Automatic Parsing of JSON Query & Mutation Variables into InputType Structs 224 | 225 | To ease working with input parameters custom structs can be registered to be instantiated from the json params of query and mutation requests. Given the schema from above one can define a PostInput struct as follows 226 | 227 | ```cr 228 | struct PostInput < GraphQL::Schema::InputType 229 | JSON.mapping( 230 | author: String, 231 | title: String, 232 | body: String 233 | ) 234 | end 235 | ``` 236 | 237 | and register it in the schema like: 238 | 239 | ```cr 240 | schema.add_input_type("PostInput", PostInput) 241 | ``` 242 | 243 | Now the argument `post` which is expected to be a GraphQL InputType `PostInput` will be automatically parsed into a crystal `PostInput`-struct. Thus the code in the `post` mutation callback becomes more simple: 244 | 245 | ```cr 246 | module MutationType 247 | include GraphQL::ObjectType 248 | extend self 249 | 250 | field :post do |args| 251 | input = args["post"].as(PostInput) 252 | 253 | author = USERS.find &.name.==(input.author) || 254 | raise "author doesn't exist" 255 | 256 | POSTS << Post.new(input.title, input.body, author) 257 | POSTS.last 258 | end 259 | end 260 | ``` 261 | 262 | ### Custom Context Types 263 | 264 | Custom context types can be used to pass additional information to the object type's field resolves. An example can be found [here](spec/support/custom_context_schema.cr). 265 | 266 | A custom context type should inherit from `GraphQL::Schema::Context` and therefore be initialized with the served schema and a max_depth. 267 | 268 | ```cr 269 | GraphQL::Schema::Schema#execute(query_string, query_arguments = nil, context = GraphQL::Schema::Context.new(self, max_depth)) 270 | ``` 271 | accepts a context type as its third argument. 272 | 273 | Field resolver callbacks on object types (including top level query & mutation types) get called with the context as their second argument: 274 | ```cr 275 | field :users do |args, context| 276 | # casting to your custom type 277 | # is necessary here 278 | context = context.as(CustomContext) 279 | unless context.authenticated 280 | raise "Authentication Error" 281 | end 282 | ... 283 | end 284 | ``` 285 | 286 | ### Serving over HTTP 287 | 288 | For an example of how to serve a schema over a webserver([kemal](https://github.com/kemalcr/kemal)) see [kemal-graphql-example](https://github.com/ziprandom/kemal-graphql-example). 289 | 290 | ## Development 291 | 292 | run tests with 293 | 294 | ``` 295 | crystal spec 296 | ``` 297 | 298 | ## Contributing 299 | 300 | 1. Fork it ( https://github.com/ziprandom/graphql-crystal/fork ) 301 | 2. Create your feature branch (git checkout -b my-new-feature) 302 | 3. Commit your changes (git commit -am 'Add some feature') 303 | 4. Push to the branch (git push origin my-new-feature) 304 | 5. Create a new Pull Request 305 | 306 | ## Contributors 307 | 308 | - [ziprandom](https://github.com/ziprandom) - creator, maintainer 309 | -------------------------------------------------------------------------------- /benchmark/compare_benchmarks.cr: -------------------------------------------------------------------------------- 1 | # use compile time finalized version 2 | require "../src/graphql-crystal" 3 | require "./lib/libragphqlparserC" 4 | require "benchmark" 5 | 6 | schema_string = <<-schema_string 7 | schema { 8 | query: QueryType 9 | } 10 | 11 | # One of the Movies 12 | enum Episode { 13 | # Episode IV: A New Hope 14 | NEWHOPE 15 | # Episode V: The Empire Strikes Back 16 | EMPIRE 17 | # Episode VI: Return of the Jedi 18 | JEDI 19 | } 20 | 21 | type QueryType { 22 | # Get the main hero of an episode 23 | hero(episode: Episode): Character 24 | # Get Humans by Id 25 | humans(ids: [String]): [Human] 26 | # Get a Human by Id 27 | human(id: String!): Human 28 | # Get a Droid by Id 29 | droid(id: String!): Droid 30 | } 31 | 32 | # A Star Wars Character 33 | interface Character { 34 | # The id of the character 35 | id: String 36 | 37 | # The name of the character 38 | name: String 39 | 40 | # The friends of the character or 41 | # an empty list if the have none 42 | friends: [Character] 43 | # Which movies they appear in 44 | appearsIn: [Episode] 45 | # All secrets about their past 46 | secretBackstory: String 47 | } 48 | 49 | # A humanoid Star Wars Character 50 | type Human implements Character { 51 | # the home planet of the 52 | # human, or null if unknown 53 | homePlanet: String 54 | } 55 | 56 | # A robotic Star Wars Character 57 | type Droid implements Character { 58 | # The primary function of the droid 59 | primaryFunction: String 60 | } 61 | 62 | schema_string 63 | 64 | query_string = <<-query_string 65 | { 66 | firstUser: user(id: 0) { 67 | ... userFields 68 | }, 69 | secondUser: user(id: 1) { 70 | ... userFields 71 | } 72 | } 73 | fragment userFields on User { 74 | id, name 75 | } 76 | query_string 77 | 78 | # Parse the Schema to a Document ASTNode 79 | Benchmark.ips(warmup: 4, calculation: 10) do |x| 80 | x.report("SCHEMA String: c implementation from facebook: ") { 81 | GraphQLParser.parse_string(schema_string, nil) 82 | } 83 | 84 | x.report("SCHEMA String: dotnet based implementation: ") { 85 | GraphQL::Language::Parser.new( 86 | GraphQL::Language::Lexer.new 87 | ).parse(schema_string) 88 | } 89 | end 90 | 91 | # Parse the Schema to a Document ASTNode 92 | Benchmark.ips(warmup: 4, calculation: 10) do |x| 93 | x.report("QUERY String: c implementation from facebook: ") { 94 | GraphQLParser.parse_string(query_string, nil) 95 | } 96 | 97 | x.report("QUERY String: dotnet based implementation: ") { 98 | GraphQL::Language::Parser.new( 99 | GraphQL::Language::Lexer.new 100 | ).parse(query_string) 101 | } 102 | end 103 | -------------------------------------------------------------------------------- /benchmark/lib/libragphqlparser.cr: -------------------------------------------------------------------------------- 1 | # bare skeleton used to create bindings for 2 | # facebooks libgraphqlparser with crystal_lib 3 | @[Include("GraphQLParser.h", prefix: %w(graphql_), flags: "-I/usr/local/include/graphqlparser/c/")] 4 | @[Link(GraphQLParser)] 5 | lib GraphQLParser 6 | end 7 | -------------------------------------------------------------------------------- /benchmark/lib/libragphqlparserC.cr: -------------------------------------------------------------------------------- 1 | @[Link("graphqlparser")] 2 | lib GraphQLParser 3 | fun parse_string = graphql_parse_string(text : LibC::Char*, error : LibC::Char**) : GraphQlAstNode* 4 | alias GraphQlAstNode = Void 5 | fun parse_string_with_experimental_schema_support = graphql_parse_string_with_experimental_schema_support(text : LibC::Char*, error : LibC::Char**) : GraphQlAstNode* 6 | fun parse_file = graphql_parse_file(file : File*, error : LibC::Char**) : GraphQlAstNode* 7 | 8 | struct X_IoFile 9 | _flags : LibC::Int 10 | _io_read_ptr : LibC::Char* 11 | _io_read_end : LibC::Char* 12 | _io_read_base : LibC::Char* 13 | _io_write_base : LibC::Char* 14 | _io_write_ptr : LibC::Char* 15 | _io_write_end : LibC::Char* 16 | _io_buf_base : LibC::Char* 17 | _io_buf_end : LibC::Char* 18 | _io_save_base : LibC::Char* 19 | _io_backup_base : LibC::Char* 20 | _io_save_end : LibC::Char* 21 | _markers : X_IoMarker* 22 | _chain : X_IoFile* 23 | _fileno : LibC::Int 24 | _flags2 : LibC::Int 25 | _old_offset : X__OffT 26 | _cur_column : LibC::UShort 27 | _vtable_offset : LibC::Char 28 | _shortbuf : LibC::Char[1] 29 | _lock : X_IoLockT* 30 | _offset : X__Off64T 31 | __pad1 : Void* 32 | __pad2 : Void* 33 | __pad3 : Void* 34 | __pad4 : Void* 35 | __pad5 : LibC::Int 36 | _mode : LibC::Int 37 | _unused2 : LibC::Char 38 | end 39 | 40 | type File = X_IoFile 41 | 42 | struct X_IoMarker 43 | _next : X_IoMarker* 44 | _sbuf : X_IoFile* 45 | _pos : LibC::Int 46 | end 47 | 48 | alias X__OffT = LibC::Long 49 | alias X_IoLockT = Void 50 | alias X__Off64T = LibC::Long 51 | fun parse_file_with_experimental_schema_support = graphql_parse_file_with_experimental_schema_support(file : File*, error : LibC::Char**) : GraphQlAstNode* 52 | fun error_free = graphql_error_free(error : LibC::Char*) 53 | end 54 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #FFFFFF; 3 | position: relative; 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | font-family: "Avenir", "Tahoma", "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; 13 | color: #333; 14 | line-height: 1.5; 15 | } 16 | 17 | a { 18 | color: #263F6C; 19 | } 20 | 21 | a:visited { 22 | color: #112750; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin: 35px 0 25px; 27 | color: #444444; 28 | } 29 | 30 | h1.type-name { 31 | color: #47266E; 32 | margin: 20px 0 30px; 33 | background-color: #F8F8F8; 34 | padding: 10px 12px; 35 | border: 1px solid #EBEBEB; 36 | border-radius: 2px; 37 | } 38 | 39 | h2 { 40 | border-bottom: 1px solid #E6E6E6; 41 | padding-bottom: 5px; 42 | } 43 | 44 | body { 45 | display: flex; 46 | } 47 | 48 | .sidebar, .main-content { 49 | overflow: auto; 50 | } 51 | 52 | .sidebar { 53 | width: 30em; 54 | color: #F8F4FD; 55 | background-color: #2E1052; 56 | padding: 0 0 30px; 57 | box-shadow: inset -3px 0 4px rgba(0,0,0,.35); 58 | line-height: 1.2; 59 | } 60 | 61 | .sidebar .search-box { 62 | padding: 8px 9px; 63 | } 64 | 65 | .sidebar input { 66 | display: block; 67 | box-sizing: border-box; 68 | margin: 0; 69 | padding: 5px; 70 | font: inherit; 71 | font-family: inherit; 72 | line-height: 1.2; 73 | width: 100%; 74 | border: 0; 75 | outline: 0; 76 | border-radius: 2px; 77 | box-shadow: 0px 3px 5px rgba(0,0,0,.25); 78 | transition: box-shadow .12s; 79 | } 80 | 81 | .sidebar input:focus { 82 | box-shadow: 0px 5px 6px rgba(0,0,0,.5); 83 | } 84 | 85 | .sidebar input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 86 | color: #C8C8C8; 87 | font-size: 14px; 88 | text-indent: 2px; 89 | } 90 | 91 | .sidebar input::-moz-placeholder { /* Firefox 19+ */ 92 | color: #C8C8C8; 93 | font-size: 14px; 94 | text-indent: 2px; 95 | } 96 | 97 | .sidebar input:-ms-input-placeholder { /* IE 10+ */ 98 | color: #C8C8C8; 99 | font-size: 14px; 100 | text-indent: 2px; 101 | } 102 | 103 | .sidebar input:-moz-placeholder { /* Firefox 18- */ 104 | color: #C8C8C8; 105 | font-size: 14px; 106 | text-indent: 2px; 107 | } 108 | 109 | .sidebar ul { 110 | margin: 0; 111 | padding: 0; 112 | list-style: none outside; 113 | } 114 | 115 | .sidebar li { 116 | display: block; 117 | position: relative; 118 | } 119 | 120 | .types-list li.hide { 121 | display: none; 122 | } 123 | 124 | .sidebar a { 125 | text-decoration: none; 126 | color: inherit; 127 | transition: color .14s; 128 | } 129 | .types-list a { 130 | display: block; 131 | padding: 5px 15px 5px 30px; 132 | } 133 | 134 | .types-list { 135 | display: block; 136 | } 137 | 138 | .sidebar a:focus { 139 | outline: 1px solid #D1B7F1; 140 | } 141 | 142 | .types-list a { 143 | padding: 5px 15px 5px 30px; 144 | } 145 | 146 | .sidebar .current > a, 147 | .sidebar a:hover { 148 | color: #866BA6; 149 | } 150 | 151 | .repository-links { 152 | padding: 5px 15px 5px 30px; 153 | } 154 | 155 | .types-list li ul { 156 | overflow: hidden; 157 | height: 0; 158 | max-height: 0; 159 | transition: 1s ease-in-out; 160 | } 161 | 162 | .types-list li.parent { 163 | padding-left: 30px; 164 | } 165 | 166 | .types-list li.parent::before { 167 | box-sizing: border-box; 168 | content: "▼"; 169 | display: block; 170 | width: 30px; 171 | height: 30px; 172 | position: absolute; 173 | top: 0; 174 | left: 0; 175 | text-align: center; 176 | color: white; 177 | font-size: 8px; 178 | line-height: 30px; 179 | transform: rotateZ(-90deg); 180 | cursor: pointer; 181 | transition: .2s linear; 182 | } 183 | 184 | 185 | .types-list li.parent > a { 186 | padding-left: 0; 187 | } 188 | 189 | .types-list li.parent.open::before { 190 | transform: rotateZ(0); 191 | } 192 | 193 | .types-list li.open > ul { 194 | height: auto; 195 | max-height: 1000em; 196 | } 197 | 198 | .main-content { 199 | padding: 0 30px 30px 30px; 200 | width: 100%; 201 | } 202 | 203 | .kind { 204 | font-size: 60%; 205 | color: #866BA6; 206 | } 207 | 208 | .superclass-hierarchy { 209 | margin: -15px 0 30px 0; 210 | padding: 0; 211 | list-style: none outside; 212 | font-size: 80%; 213 | } 214 | 215 | .superclass-hierarchy .superclass { 216 | display: inline-block; 217 | margin: 0 7px 0 0; 218 | padding: 0; 219 | } 220 | 221 | .superclass-hierarchy .superclass + .superclass::before { 222 | content: "<"; 223 | margin-right: 7px; 224 | } 225 | 226 | .other-types-list li { 227 | display: inline-block; 228 | } 229 | 230 | .other-types-list, 231 | .list-summary { 232 | margin: 0 0 30px 0; 233 | padding: 0; 234 | list-style: none outside; 235 | } 236 | 237 | .entry-const { 238 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 239 | } 240 | 241 | .entry-const code { 242 | white-space: pre-wrap; 243 | } 244 | 245 | .entry-summary { 246 | padding-bottom: 4px; 247 | } 248 | 249 | .superclass-hierarchy .superclass a, 250 | .other-type a, 251 | .entry-summary .signature { 252 | padding: 4px 8px; 253 | margin-bottom: 4px; 254 | display: inline-block; 255 | background-color: #f8f8f8; 256 | color: #47266E; 257 | border: 1px solid #f0f0f0; 258 | text-decoration: none; 259 | border-radius: 3px; 260 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 261 | transition: background .15s, border-color .15s; 262 | } 263 | 264 | .superclass-hierarchy .superclass a:hover, 265 | .other-type a:hover, 266 | .entry-summary .signature:hover { 267 | background: #D5CAE3; 268 | border-color: #624288; 269 | } 270 | 271 | .entry-summary .summary { 272 | padding-left: 32px; 273 | } 274 | 275 | .entry-summary .summary p { 276 | margin: 12px 0 16px; 277 | } 278 | 279 | .entry-summary a { 280 | text-decoration: none; 281 | } 282 | 283 | .entry-detail { 284 | padding: 30px 0; 285 | } 286 | 287 | .entry-detail .signature { 288 | position: relative; 289 | padding: 5px 15px; 290 | margin-bottom: 10px; 291 | display: block; 292 | border-radius: 5px; 293 | background-color: #f8f8f8; 294 | color: #47266E; 295 | border: 1px solid #f0f0f0; 296 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 297 | transition: .2s ease-in-out; 298 | } 299 | 300 | .entry-detail:target .signature { 301 | background-color: #D5CAE3; 302 | border: 1px solid #624288; 303 | } 304 | 305 | .entry-detail .signature .method-permalink { 306 | position: absolute; 307 | top: 0; 308 | left: -35px; 309 | padding: 5px 15px; 310 | text-decoration: none; 311 | font-weight: bold; 312 | color: #624288; 313 | opacity: .4; 314 | transition: opacity .2s; 315 | } 316 | 317 | .entry-detail .signature .method-permalink:hover { 318 | opacity: 1; 319 | } 320 | 321 | .entry-detail:target .signature .method-permalink { 322 | opacity: 1; 323 | } 324 | 325 | .methods-inherited { 326 | padding-right: 10%; 327 | line-height: 1.5em; 328 | } 329 | 330 | .methods-inherited h3 { 331 | margin-bottom: 4px; 332 | } 333 | 334 | .methods-inherited a { 335 | display: inline-block; 336 | text-decoration: none; 337 | color: #47266E; 338 | } 339 | 340 | .methods-inherited a:hover { 341 | text-decoration: underline; 342 | color: #6C518B; 343 | } 344 | 345 | .methods-inherited .tooltip>span { 346 | background: #D5CAE3; 347 | padding: 4px 8px; 348 | border-radius: 3px; 349 | margin: -4px -8px; 350 | } 351 | 352 | .methods-inherited .tooltip * { 353 | color: #47266E; 354 | } 355 | 356 | pre { 357 | padding: 10px 20px; 358 | margin-top: 4px; 359 | border-radius: 3px; 360 | line-height: 1.45; 361 | overflow: auto; 362 | color: #333; 363 | background: #fdfdfd; 364 | font-size: 14px; 365 | border: 1px solid #eee; 366 | } 367 | 368 | code { 369 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 370 | } 371 | 372 | :not(pre) > code { 373 | background-color: rgba(40,35,30,0.05); 374 | padding: 0.2em 0.4em; 375 | font-size: 85%; 376 | border-radius: 3px; 377 | } 378 | 379 | span.flag { 380 | padding: 2px 4px 1px; 381 | border-radius: 3px; 382 | margin-right: 3px; 383 | font-size: 11px; 384 | border: 1px solid transparent; 385 | } 386 | 387 | span.flag.orange { 388 | background-color: #EE8737; 389 | color: #FCEBDD; 390 | border-color: #EB7317; 391 | } 392 | 393 | span.flag.yellow { 394 | background-color: #E4B91C; 395 | color: #FCF8E8; 396 | border-color: #B69115; 397 | } 398 | 399 | span.flag.green { 400 | background-color: #469C14; 401 | color: #E2F9D3; 402 | border-color: #34700E; 403 | } 404 | 405 | span.flag.red { 406 | background-color: #BF1919; 407 | color: #F9ECEC; 408 | border-color: #822C2C; 409 | } 410 | 411 | span.flag.purple { 412 | background-color: #2E1052; 413 | color: #ECE1F9; 414 | border-color: #1F0B37; 415 | } 416 | 417 | .tooltip>span { 418 | position: absolute; 419 | opacity: 0; 420 | display: none; 421 | pointer-events: none; 422 | } 423 | 424 | .tooltip:hover>span { 425 | display: inline-block; 426 | opacity: 1; 427 | } 428 | 429 | .c { 430 | color: #969896; 431 | } 432 | 433 | .n { 434 | color: #0086b3; 435 | } 436 | 437 | .t { 438 | color: #0086b3; 439 | } 440 | 441 | .s { 442 | color: #183691; 443 | } 444 | 445 | .i { 446 | color: #7f5030; 447 | } 448 | 449 | .k { 450 | color: #a71d5d; 451 | } 452 | 453 | .o { 454 | color: #a71d5d; 455 | } 456 | 457 | .m { 458 | color: #795da3; 459 | } 460 | 461 | .hidden { 462 | display: none; 463 | } 464 | .search-results { 465 | font-size: 90%; 466 | line-height: 1.3; 467 | } 468 | 469 | .search-results mark { 470 | color: inherit; 471 | background: transparent; 472 | font-weight: bold; 473 | } 474 | .search-result { 475 | padding: 5px 8px 5px 5px; 476 | cursor: pointer; 477 | border-left: 5px solid transparent; 478 | transform: translateX(-3px); 479 | transition: all .2s, background-color 0s, border .02s; 480 | min-height: 3.2em; 481 | } 482 | .search-result.current { 483 | border-left-color: #ddd; 484 | background-color: rgba(200,200,200,0.4); 485 | transform: translateX(0); 486 | transition: all .2s, background-color .5s, border 0s; 487 | } 488 | .search-result.current:hover, 489 | .search-result.current:focus { 490 | border-left-color: #866BA6; 491 | } 492 | .search-result:not(.current):nth-child(2n) { 493 | background-color: rgba(255,255,255,.06); 494 | } 495 | .search-result__title { 496 | font-size: 105%; 497 | word-break: break-all; 498 | line-height: 1.1; 499 | padding: 3px 0; 500 | } 501 | .search-result__title strong { 502 | font-weight: normal; 503 | } 504 | .search-results .search-result__title > a { 505 | padding: 0; 506 | display: block; 507 | } 508 | .search-result__title > a > .args { 509 | color: #dddddd; 510 | font-weight: 300; 511 | transition: inherit; 512 | font-size: 88%; 513 | line-height: 1.2; 514 | letter-spacing: -.02em; 515 | } 516 | .search-result__title > a > .args * { 517 | color: inherit; 518 | } 519 | 520 | .search-result a, 521 | .search-result a:hover { 522 | color: inherit; 523 | } 524 | .search-result:not(.current):hover .search-result__title > a, 525 | .search-result:not(.current):focus .search-result__title > a, 526 | .search-result__title > a:focus { 527 | color: #866BA6; 528 | } 529 | .search-result:not(.current):hover .args, 530 | .search-result:not(.current):focus .args { 531 | color: #6a5a7d; 532 | } 533 | 534 | .search-result__type { 535 | color: #e8e8e8; 536 | font-weight: 300; 537 | } 538 | .search-result__doc { 539 | color: #bbbbbb; 540 | font-size: 90%; 541 | } 542 | .search-result__doc p { 543 | margin: 0; 544 | text-overflow: ellipsis; 545 | display: -webkit-box; 546 | -webkit-box-orient: vertical; 547 | -webkit-line-clamp: 2; 548 | overflow: hidden; 549 | line-height: 1.2em; 550 | max-height: 2.4em; 551 | } 552 | 553 | .js-modal-visible .modal-background { 554 | display: flex; 555 | } 556 | .main-content { 557 | position: relative; 558 | } 559 | .modal-background { 560 | position: absolute; 561 | display: none; 562 | height: 100%; 563 | width: 100%; 564 | background: rgba(120,120,120,.4); 565 | z-index: 100; 566 | align-items: center; 567 | justify-content: center; 568 | } 569 | .usage-modal { 570 | max-width: 90%; 571 | background: #fff; 572 | border: 2px solid #ccc; 573 | border-radius: 9px; 574 | padding: 5px 15px 20px; 575 | min-width: 50%; 576 | color: #555; 577 | position: relative; 578 | transform: scale(.5); 579 | transition: transform 200ms; 580 | } 581 | .js-modal-visible .usage-modal { 582 | transform: scale(1); 583 | } 584 | .usage-modal > .close-button { 585 | position: absolute; 586 | right: 15px; 587 | top: 8px; 588 | color: #aaa; 589 | font-size: 27px; 590 | cursor: pointer; 591 | } 592 | .usage-modal > .close-button:hover { 593 | text-shadow: 2px 2px 2px #ccc; 594 | color: #999; 595 | } 596 | .modal-title { 597 | margin: 0; 598 | text-align: center; 599 | font-weight: normal; 600 | color: #666; 601 | border-bottom: 2px solid #ddd; 602 | padding: 10px; 603 | } 604 | .usage-list { 605 | padding: 0; 606 | margin: 13px; 607 | } 608 | .usage-list > li { 609 | padding: 5px 2px; 610 | overflow: auto; 611 | padding-left: 100px; 612 | min-width: 12em; 613 | } 614 | .usage-modal kbd { 615 | background: #eee; 616 | border: 1px solid #ccc; 617 | border-bottom-width: 2px; 618 | border-radius: 3px; 619 | padding: 3px 8px; 620 | font-family: monospace; 621 | margin-right: 2px; 622 | display: inline-block; 623 | } 624 | .usage-key { 625 | float: left; 626 | clear: left; 627 | margin-left: -100px; 628 | margin-right: 12px; 629 | } 630 | .doc-inherited { 631 | font-weight: bold; 632 | } 633 | -------------------------------------------------------------------------------- /example/simple_blog_example.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "../src/graphql-crystal" 3 | require "secure_random" 4 | require "benchmark" 5 | 6 | module BlogExample 7 | enum UserRole 8 | Author 9 | Reader 10 | Admin 11 | end 12 | 13 | # 14 | # Lets create a simple Blog Scenario where there exist Users, Posts and Comments 15 | # 16 | # First we define 4 classes to represent our Model: User, Content, Post < Content & Comment < Content 17 | # 18 | 19 | class User 20 | getter :id, :first_name, :last_name, :role 21 | 22 | def initialize(@id : String, @first_name : String, @last_name : String, @role : UserRole); end 23 | end 24 | 25 | abstract class Content 26 | # 27 | # due to https://github.com/crystal-lang/crystal/issues/4580 28 | # we have to include the ObjectType module at the first definition of Content 29 | # in order for the field macro to work on child classes. Once this is fixed the 30 | # arbitrary classes can declared as GraphQL Object types easily via monkey Patching 31 | include GraphQL::ObjectType 32 | @id : String 33 | @body : String 34 | @author : User 35 | 36 | def initialize(@id, @body, @author); end 37 | end 38 | 39 | class Post < Content 40 | getter :id, :author, :title, :body 41 | 42 | def initialize(@id : String, @author : User, @title : String, @body : String); end 43 | end 44 | 45 | class Comment < Content 46 | getter :id, :author, :post, :body 47 | 48 | def initialize(@id : String, @author : User, @post : Post, @body : String); end 49 | end 50 | 51 | # 52 | # and create some fixtures to work with 53 | # 54 | # 55 | # and create some fixtures to work with 56 | # 57 | USERS = [ 58 | {id: SecureRandom.uuid, first_name: "Bob", last_name: "Bobson", role: UserRole::Author}, 59 | {id: SecureRandom.uuid, first_name: "Alice", last_name: "Alicen", role: UserRole::Admin}, 60 | {id: SecureRandom.uuid, first_name: "Grace", last_name: "Graham", role: UserRole::Reader}, 61 | ].map { |args| User.new **args } 62 | 63 | POSTS = [ 64 | {id: SecureRandom.uuid, author: USERS[0], title: "GraphQL for Dummies", body: "GraphQL is pretty simple."}, 65 | {id: SecureRandom.uuid, author: USERS[0], title: "REST vs. GraphQL", body: "GraphQL has certain advantages over REST."}, 66 | {id: SecureRandom.uuid, author: USERS[1], title: "The Crystal Programming Language ", body: "The nicest syntax on the planet now comes with typesafety, performance and parallelisation support(ójala!)"}, 67 | ].map { |args| Post.new **args } 68 | 69 | COMMENTS = [ 70 | {id: SecureRandom.uuid, author: USERS[2], post: POSTS[1], body: "I like rest more!"}, 71 | {id: SecureRandom.uuid, author: USERS[2], post: POSTS[1], body: "But think of all the possibilities with GraphQL!"}, 72 | {id: SecureRandom.uuid, author: USERS[1], post: POSTS[2], body: "When will I finally have static compilation support?"}, 73 | ].map { |args| Comment.new **args } 74 | 75 | # 76 | # Now we define our Schema 77 | # 78 | 79 | graphql_schema_definition = <<-graphql_schema 80 | schema { 81 | query: QueryType, 82 | mutation: MutationType 83 | } 84 | 85 | type QueryType { 86 | # retrieve a user by id 87 | user(id: ID!): User 88 | # retrieve a post by id 89 | post(id: ID!): Post 90 | # get all posts 91 | posts: [Post!] 92 | } 93 | 94 | type MutationType { 95 | # create a new post 96 | post(payload: PostInput!): Post 97 | # create a new comment 98 | comment(payload: CommentInput!): Comment 99 | } 100 | 101 | # Input format for 102 | # new Posts 103 | input PostInput { 104 | # title for the new post 105 | title: String! 106 | # body for the new post 107 | body: String! 108 | # id of the posts author 109 | authorId: ID! 110 | } 111 | 112 | # Input format for 113 | # new Comments 114 | input CommentInput { 115 | # id of the post on 116 | # which is being commented 117 | postId: ID! 118 | # id of the comments author 119 | authorId: ID! 120 | # the comments text 121 | body: String! 122 | } 123 | 124 | # Possible roles 125 | # for users in the system 126 | enum UserRole { 127 | # A user with 128 | # readonly access to 129 | # the Content of the system 130 | Reader 131 | # A user with read 132 | # & write access 133 | Author 134 | # A administrator 135 | # of the system 136 | Admin 137 | } 138 | 139 | # Types identified by a 140 | # unique ID 141 | interface UniqueId { 142 | # the unique idenfifier 143 | # for this entity 144 | id: ID! 145 | } 146 | 147 | # A User 148 | type User implements UniqueId { 149 | # users first name 150 | firstName: String! 151 | # users last name 152 | lastName: String! 153 | # full name string for the user 154 | fullName: String! 155 | # users role 156 | role: UserRole! 157 | # posts published 158 | # by this user 159 | posts: [Post!] 160 | # total number of posts 161 | # published by this user 162 | postsCount: Int! 163 | } 164 | 165 | # Text content 166 | interface Content { 167 | # text body of this entity 168 | body: String! 169 | # author of this entity 170 | author: User! 171 | } 172 | 173 | # A post in the system 174 | type Post implements UniqueId, Content { 175 | # title of this post 176 | title: String! 177 | } 178 | 179 | # A comment on a post 180 | type Comment implements UniqueId, Content { 181 | # post on which this 182 | # comment was made 183 | post: Post! 184 | } 185 | graphql_schema 186 | 187 | # load it 188 | schema = GraphQL::Schema.from_schema(graphql_schema_definition) 189 | 190 | module UniqueId 191 | macro included 192 | field :id 193 | end 194 | end 195 | 196 | # Here we reopen the classes 197 | # of our application model and 198 | # enhance them to act as GraphQL Object Types via the GraphQL::ObjectType 199 | # and define the available fields via the field macro. fields resolve to an 200 | # instanc emethod of the same name unless stated otherwise 201 | abstract class Content 202 | # this doesn't work here due to https://github.com/crystal-lang/crystal/issues/4580 203 | # so we included the module at the first declaration of the Content class above 204 | # include GraphQL::ObjectType 205 | include UniqueId 206 | field :body 207 | field :author 208 | end 209 | 210 | # you see it works nicely with inheritance 211 | class Post 212 | field :title 213 | end 214 | 215 | class Comment 216 | field :post 217 | end 218 | 219 | # 220 | # Here we make use of custom callbacks 221 | # to convert snake_case to camelCase 222 | # and add virtual accessors 223 | # 224 | class User 225 | include GraphQL::ObjectType 226 | include UniqueId 227 | field :firstName { first_name } 228 | field :lastName { last_name } 229 | field :fullName { "#{@first_name} #{@last_name}" } 230 | field :posts { POSTS.select &.author.==(self) } 231 | field :postsCount { POSTS.select(&.author.==(self)).size } 232 | field :role 233 | end 234 | 235 | module QueryType 236 | include GraphQL::ObjectType 237 | extend self 238 | 239 | field "posts" { POSTS } 240 | 241 | field "user" do |args| 242 | USERS.find(&.id.==(args["id"])) 243 | end 244 | 245 | field "post" do |args| 246 | POSTS.find(&.id.==(args["id"])) 247 | end 248 | end 249 | 250 | module MutationType 251 | include GraphQL::ObjectType 252 | extend self 253 | 254 | field "post" do |args| 255 | payload = args["payload"].as(Hash) 256 | 257 | author = USERS.find(&.id.==(payload["authorId"])) 258 | raise "authorId doesn't exist!" unless author 259 | 260 | post = Post.new( 261 | id: SecureRandom.uuid, author: author, 262 | title: payload["title"].as(String), body: payload["body"].as(String) 263 | ) 264 | 265 | POSTS << post 266 | post 267 | end 268 | 269 | field "comment" do |args| 270 | payload = args["payload"].as(Hash) 271 | 272 | author = USERS.find(&.id.==(payload["authorId"])) 273 | raise "authorId doesn't exist!" unless author 274 | 275 | post = POSTS.find(&.id.==(payload["postId"])) 276 | raise "postId doesn't exist!" unless post 277 | 278 | comment = Comment.new( 279 | id: SecureRandom.uuid, author: author, 280 | post: post, body: payload["body"].as(String) 281 | ) 282 | COMMENTS << comment 283 | comment 284 | end 285 | end 286 | 287 | # 288 | # finally we assign the top level query & mutation types 289 | # 290 | schema.query_resolver = QueryType 291 | schema.mutation_resolver = MutationType 292 | end 293 | 294 | # 295 | # execute a simple query 296 | # 297 | puts schema.execute("{ posts {id title body author {fullName}} }").to_pretty_json 298 | 299 | # 300 | # a simple introspection query: 301 | # 302 | puts schema.execute("{ __type(name: \"Post\") { fields { name description type { kind } } } }").to_pretty_json 303 | 304 | # 305 | # create a Post via a mutation 306 | # 307 | mutation_string = %{ 308 | mutation CreatePost($payload: PostInput) { 309 | post(payload: $payload) { 310 | id 311 | title 312 | body 313 | author { 314 | postsCount 315 | } 316 | } 317 | } 318 | } 319 | 320 | mutation_args = { 321 | "payload" => { 322 | "title" => "Using Crystal 1.0 in Production", 323 | "body" => "would be the most wonderful thing", 324 | "authorId" => BlogExample::USERS.first.id, 325 | }, 326 | } 327 | 328 | puts schema.execute(mutation_string, mutation_args).to_pretty_json 329 | 330 | # 331 | # comment on the post we just created 332 | # 333 | mutation_string = %{ 334 | mutation CreateComment($payload: CommentInput) { 335 | comment(payload: $payload) { 336 | id 337 | body 338 | } 339 | } 340 | } 341 | 342 | mutation_args = { 343 | "payload" => { 344 | "postId" => BlogExample::POSTS.last.id, 345 | "body" => "would be the most wonderful thing", 346 | "authorId" => BlogExample::USERS[1].id, 347 | }, 348 | } 349 | 350 | puts schema.execute(mutation_string, mutation_args).to_pretty_json 351 | 352 | # lets create lots of Comments 353 | 354 | Benchmark.ips do |x| 355 | x.report("commenting on that post") do 356 | schema.execute(mutation_string, mutation_args).to_json 357 | end 358 | end 359 | -------------------------------------------------------------------------------- /example/simple_example.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/graphql-crystal" 3 | 4 | # 5 | # Given this simple domain model of users and posts 6 | # 7 | class User 8 | property name 9 | 10 | def initialize(@name : String); end 11 | end 12 | 13 | class Post 14 | property :title, :body, :author 15 | 16 | def initialize(@title : String, @body : String, @author : User); end 17 | end 18 | 19 | POSTS = [] of Post 20 | USERS = [User.new("Alice"), User.new("Bob")] 21 | 22 | # 23 | # We can instantiate a GraphQL Schema Validator/Executor 24 | # by parsing from a graphql schema definition 25 | # 26 | 27 | schema = GraphQL::Schema.from_schema( 28 | %{ 29 | schema { 30 | query: QueryType, 31 | mutation: MutationType 32 | } 33 | 34 | type QueryType { 35 | posts: [PostType] 36 | users: [UserType] 37 | user(name: String!): User 38 | } 39 | 40 | type MutationType { 41 | post(post: PostInput) : PostType 42 | } 43 | 44 | input PostInput { 45 | author: String! 46 | title: String! 47 | body: String! 48 | } 49 | 50 | type UserType { 51 | name: String 52 | posts: [PostType] 53 | } 54 | 55 | type PostType { 56 | author: UserType 57 | title: String 58 | body: String 59 | } 60 | } 61 | ) 62 | 63 | # 64 | # Then we create the backing types by including the 65 | # GraphQL::ObjectType and defining the fields 66 | 67 | # reopening User and Post class 68 | class User 69 | include GraphQL::ObjectType 70 | # defaults to the method of 71 | # the same name without block 72 | field :name 73 | 74 | field :posts do 75 | POSTS.select &.author.==(self) 76 | end 77 | end 78 | 79 | class Post 80 | include GraphQL::ObjectType 81 | field :title 82 | field :body 83 | field :author 84 | end 85 | 86 | # 87 | # A Struct to hold input parameters 88 | # 89 | struct PostInput < GraphQL::Schema::InputType 90 | JSON.mapping( 91 | author: String, 92 | title: String, 93 | body: String 94 | ) 95 | end 96 | 97 | schema.add_input_type("PostInput", PostInput) 98 | 99 | # 100 | # Then we define the top level queries 101 | # extending self to make the module 102 | # act as a singleton model 103 | module QueryType 104 | include GraphQL::ObjectType 105 | extend self 106 | 107 | field :users do 108 | USERS 109 | end 110 | 111 | field :user do |args| 112 | USERS.find(&.name.==(args["name"].as(String))) || raise "no user by that name" 113 | end 114 | 115 | field :posts do 116 | POSTS 117 | end 118 | end 119 | 120 | module MutationType 121 | include GraphQL::ObjectType 122 | extend self 123 | 124 | field :post do |args| 125 | input = args["post"].as(PostInput) 126 | 127 | author = USERS.find &.name.==(input.author) || 128 | raise "author doesn't exist" 129 | 130 | POSTS << Post.new(input.title, input.body, author) 131 | POSTS.last 132 | end 133 | end 134 | 135 | # 136 | # finally set the top level Object Types 137 | # on the schema 138 | schema.query_resolver = QueryType 139 | schema.mutation_resolver = MutationType 140 | 141 | describe "my graphql schema" do 142 | it "does queries" do 143 | schema.execute("{ users { name posts } }") 144 | .should eq ({ 145 | "data" => { 146 | "users" => [ 147 | { 148 | "name" => "Alice", 149 | "posts" => [] of String, 150 | }, 151 | { 152 | "name" => "Bob", 153 | "posts" => [] of String, 154 | }, 155 | ], 156 | }, 157 | }) 158 | end 159 | 160 | it "does mutations" do 161 | mutation_string = %{ 162 | mutation post($post: PostInput) { 163 | post(post: $post) { 164 | author { 165 | name 166 | posts { title } 167 | } 168 | title 169 | body 170 | } 171 | } 172 | } 173 | 174 | payload = { 175 | "post" => { 176 | "author" => "Alice", 177 | "title" => "the long and windy road", 178 | "body" => "that leads to your door", 179 | }, 180 | } 181 | 182 | schema.execute(mutation_string, payload) 183 | .should eq ({ 184 | "data" => { 185 | "post" => { 186 | "title" => "the long and windy road", 187 | "body" => "that leads to your door", 188 | "author" => { 189 | "name" => "Alice", 190 | "posts" => [ 191 | { 192 | "title" => "the long and windy road", 193 | }, 194 | ], 195 | }, 196 | }, 197 | }, 198 | }) 199 | end 200 | 201 | it "does introspection" do 202 | query_string = %{ 203 | { 204 | __schema { 205 | types { 206 | name 207 | } 208 | } 209 | } 210 | } 211 | 212 | schema.execute(query_string) 213 | .should eq ({ 214 | "data" => { 215 | "__schema" => { 216 | "types" => [ 217 | {"name" => "String"}, 218 | {"name" => "Boolean"}, 219 | {"name" => "Int"}, 220 | {"name" => "Float"}, 221 | {"name" => "ID"}, 222 | {"name" => "QueryType"}, 223 | {"name" => "MutationType"}, 224 | {"name" => "PostInput"}, 225 | {"name" => "UserType"}, 226 | {"name" => "PostType"}, 227 | {"name" => "__Schema"}, 228 | {"name" => "__Type"}, 229 | {"name" => "__Field"}, 230 | {"name" => "__InputValue"}, 231 | {"name" => "__EnumValue"}, 232 | {"name" => "__Directive"}, 233 | {"name" => "__TypeKind"}, 234 | {"name" => "__DirectiveLocation"}, 235 | ], 236 | }, 237 | }, 238 | } 239 | ) 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: graphql-crystal 2 | version: 0.1.6 3 | 4 | authors: 5 | - ziprandom - 6 | 7 | crystal: 0.33.0 8 | 9 | license: MIT 10 | -------------------------------------------------------------------------------- /show_performance.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | crystal build --release benchmark/compare_benchmarks.cr 3 | ./compare_benchmarks 4 | -------------------------------------------------------------------------------- /spec/graphql-crystal/directive_spec.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "../spec_helper" 3 | 4 | describe GraphQL::Directive do 5 | describe GraphQL::Directives::IsDeprecated do 6 | describe "on FieldDefinition" do 7 | query_string = %{ 8 | { 9 | __type(name: "User") { 10 | fields(includeDeprecated: true) { 11 | name 12 | isDeprecated 13 | deprecationReason 14 | } 15 | } 16 | } 17 | } 18 | 19 | it "indicates the defined deprecation" do 20 | TestSchema::Schema 21 | .execute(query_string) 22 | .should eq({ 23 | "data" => { 24 | "__type" => { 25 | "fields" => [ 26 | { 27 | "name" => "address", 28 | "isDeprecated" => false, 29 | "deprecationReason" => nil, 30 | }, { 31 | "name" => "friends", 32 | "isDeprecated" => false, 33 | "deprecationReason" => nil, 34 | }, { 35 | "name" => "full_address", 36 | "isDeprecated" => false, 37 | "deprecationReason" => nil, 38 | }, { 39 | "name" => "id", 40 | "isDeprecated" => false, 41 | "deprecationReason" => nil, 42 | }, { 43 | "name" => "name", 44 | "isDeprecated" => true, 45 | "deprecationReason" => "for no apparent Reason", 46 | }, 47 | ], 48 | }, 49 | }, 50 | }) 51 | end 52 | end 53 | 54 | describe "on EnumValue" do 55 | query_string = %{ 56 | { 57 | __type(name: "City") { 58 | name 59 | enumValues { 60 | name 61 | isDeprecated 62 | deprecationReason 63 | } 64 | } 65 | } 66 | } 67 | 68 | it "indicates the defined deprecation" do 69 | TestSchema::Schema 70 | .execute(query_string) 71 | .should eq({ 72 | "data" => { 73 | "__type" => { 74 | "name" => "City", 75 | "enumValues" => [ 76 | { 77 | "name" => "London", "isDeprecated" => false, 78 | "deprecationReason" => nil, 79 | }, { 80 | "name" => "Miami", 81 | "isDeprecated" => true, 82 | "deprecationReason" => "is not a capital", 83 | }, { 84 | "name" => "CABA", 85 | "isDeprecated" => false, 86 | "deprecationReason" => nil, 87 | }, { 88 | "name" => "Istanbul", 89 | "isDeprecated" => false, 90 | "deprecationReason" => nil, 91 | }, 92 | ], 93 | }, 94 | }, 95 | }) 96 | end 97 | end 98 | end 99 | 100 | describe GraphQL::Directives::IncludeDirective do 101 | describe "on Field" do 102 | query_string = %{ 103 | query userQuery($withName: Boolean!) { 104 | user(id: 0) { 105 | id 106 | ... on User @include(if: $withName) { 107 | name 108 | } 109 | } 110 | } 111 | } 112 | 113 | it "includes if :if argument is true" do 114 | TestSchema::Schema 115 | .execute(query_string, {"withName" => true}) 116 | .should eq({ 117 | "data" => { 118 | "user" => { 119 | "id" => 0, 120 | "name" => "otto neverthere", 121 | }, 122 | }, 123 | }) 124 | end 125 | it "excludes if :if argument is false" do 126 | TestSchema::Schema 127 | .execute(query_string, {"withName" => false}) 128 | .should eq({ 129 | "data" => { 130 | "user" => { 131 | "id" => 0, 132 | }, 133 | }, 134 | }) 135 | end 136 | end 137 | 138 | describe "on inline Fragment" do 139 | query_string = %{ 140 | query userQuery($withName: Boolean!) { 141 | user(id: 0) { 142 | id 143 | ... on User @include(if: $withName) { 144 | name 145 | } 146 | } 147 | } 148 | } 149 | 150 | it "includes if :if argument is true" do 151 | TestSchema::Schema 152 | .execute(query_string, {"withName" => true}) 153 | .should eq({ 154 | "data" => { 155 | "user" => { 156 | "id" => 0, 157 | "name" => "otto neverthere", 158 | }, 159 | }, 160 | }) 161 | end 162 | 163 | it "excludes if :if argument is false" do 164 | TestSchema::Schema 165 | .execute(query_string, {"withName" => false}) 166 | .should eq({ 167 | "data" => { 168 | "user" => { 169 | "id" => 0, 170 | }, 171 | }, 172 | }) 173 | end 174 | end 175 | 176 | describe "on Fragment" do 177 | query_string = %{ 178 | query userQuery($withName: Boolean!) { 179 | user(id: 0) { 180 | id 181 | ... userName @include(if: $withName) 182 | } 183 | } 184 | fragment userName on User { 185 | name 186 | } 187 | } 188 | 189 | it "includes if :if argument is true" do 190 | TestSchema::Schema 191 | .execute(query_string, {"withName" => true}) 192 | .should eq({ 193 | "data" => { 194 | "user" => { 195 | "id" => 0, 196 | "name" => "otto neverthere", 197 | }, 198 | }, 199 | }) 200 | end 201 | 202 | it "excludes if :if argument is false" do 203 | TestSchema::Schema 204 | .execute(query_string, {"withName" => false}) 205 | .should eq({ 206 | "data" => { 207 | "user" => { 208 | "id" => 0, 209 | }, 210 | }, 211 | }) 212 | end 213 | end 214 | end 215 | 216 | describe GraphQL::Directives::SkipDirective do 217 | describe "on Field" do 218 | query_string = %{ 219 | query userQuery($skipName: Boolean!) { 220 | user(id: 0) { 221 | id 222 | ... on User @skip(if: $skipName) { 223 | name 224 | } 225 | } 226 | } 227 | } 228 | 229 | it "skips if :if argument is true" do 230 | TestSchema::Schema 231 | .execute(query_string, {"skipName" => true}) 232 | .should eq({ 233 | "data" => { 234 | "user" => { 235 | "id" => 0, 236 | }, 237 | }, 238 | }) 239 | end 240 | it "includes if :if argument is false" do 241 | TestSchema::Schema 242 | .execute(query_string, {"skipName" => false}) 243 | .should eq({ 244 | "data" => { 245 | "user" => { 246 | "id" => 0, 247 | "name" => "otto neverthere", 248 | }, 249 | }, 250 | }) 251 | end 252 | end 253 | 254 | describe "on inline Fragment" do 255 | query_string = %{ 256 | query userQuery($skipName: Boolean!) { 257 | user(id: 0) { 258 | id 259 | ... on User @skip(if: $skipName) { 260 | name 261 | } 262 | } 263 | } 264 | } 265 | 266 | it "skips if :if argument is true" do 267 | TestSchema::Schema 268 | .execute(query_string, {"skipName" => true}) 269 | .should eq({ 270 | "data" => { 271 | "user" => { 272 | "id" => 0, 273 | }, 274 | }, 275 | }) 276 | end 277 | 278 | it "includes if :if argument is false" do 279 | TestSchema::Schema 280 | .execute(query_string, {"skipName" => false}) 281 | .should eq({ 282 | "data" => { 283 | "user" => { 284 | "id" => 0, 285 | "name" => "otto neverthere", 286 | }, 287 | }, 288 | }) 289 | end 290 | end 291 | 292 | describe "on Fragment" do 293 | query_string = %{ 294 | query userQuery($skipName: Boolean!) { 295 | user(id: 0) { 296 | id 297 | ... userName @skip(if: $skipName) 298 | } 299 | } 300 | fragment userName on User { 301 | name 302 | } 303 | } 304 | 305 | it "skips if :if argument is true" do 306 | TestSchema::Schema 307 | .execute(query_string, {"skipName" => true}) 308 | .should eq({ 309 | "data" => { 310 | "user" => { 311 | "id" => 0, 312 | }, 313 | }, 314 | }) 315 | end 316 | 317 | it "includes if :if argument is false" do 318 | TestSchema::Schema 319 | .execute(query_string, {"skipName" => false}) 320 | .should eq({ 321 | "data" => { 322 | "user" => { 323 | "id" => 0, 324 | "name" => "otto neverthere", 325 | }, 326 | }, 327 | }) 328 | end 329 | end 330 | end 331 | end 332 | -------------------------------------------------------------------------------- /spec/graphql-crystal/introspection/directive_type_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe "GraphQL::Introspection::DirectiveType" do 4 | query_string = <<-query_string 5 | query getDirectives { 6 | __schema { 7 | directives { 8 | name, 9 | args { name, type { kind, name, ofType { name } } }, 10 | locations 11 | onField 12 | onFragment 13 | onOperation 14 | } 15 | } 16 | } 17 | query_string 18 | result = Dummy::Schema.execute(query_string) 19 | 20 | it "shows directive info " do 21 | expected = {"data" => { 22 | "__schema" => { 23 | "directives" => [ 24 | { 25 | "name" => "include", 26 | "args" => [ 27 | {"name" => "if", "type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "Boolean"}}}, 28 | ], 29 | "locations" => ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 30 | "onField" => true, 31 | "onFragment" => true, 32 | "onOperation" => false, 33 | }, 34 | { 35 | "name" => "skip", 36 | "args" => [ 37 | {"name" => "if", "type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "Boolean"}}}, 38 | ], 39 | "locations" => ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 40 | "onField" => true, 41 | "onFragment" => true, 42 | "onOperation" => false, 43 | }, 44 | { 45 | "name" => "deprecated", 46 | "args" => [ 47 | {"name" => "reason", "type" => {"kind" => "SCALAR", "name" => "String", "ofType" => nil}}, 48 | ], 49 | "locations" => ["FIELD_DEFINITION", "ENUM_VALUE"], 50 | "onField" => false, 51 | "onFragment" => false, 52 | "onOperation" => false, 53 | }, 54 | ], 55 | }, 56 | }} 57 | result.should eq expected 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/graphql-crystal/introspection/input_value_type_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe "GraphQL::Introspection::DirectiveType" do 4 | query_string = <<-query_string 5 | { 6 | __type(name: "DairyProductInput") { 7 | name 8 | description 9 | kind 10 | inputFields { 11 | name 12 | type { kind, name } 13 | defaultValue 14 | description 15 | } 16 | } 17 | } 18 | query_string 19 | result = Dummy::Schema.execute(query_string) 20 | 21 | it "shows directive info " do 22 | expected = { 23 | "data" => { 24 | "__type" => { 25 | "name" => "DairyProductInput", 26 | "description" => "Properties for finding a dairy product", 27 | "kind" => "INPUT_OBJECT", 28 | "inputFields" => [ 29 | { 30 | "name" => "source", "type" => {"kind" => "NON_NULL", "name" => nil}, "defaultValue" => nil, 31 | "description" => "Where it came from", 32 | }, 33 | { 34 | "name" => "originDairy", 35 | "type" => { 36 | "kind" => "SCALAR", "name" => "String", 37 | }, 38 | "defaultValue" => "\"Sugar Hollow Dairy\"", "description" => "Dairy which produced it", 39 | }, 40 | { 41 | "name" => "fatContent", 42 | "type" => { 43 | "kind" => "SCALAR", 44 | "name" => "Float", 45 | }, 46 | "defaultValue" => "0.3", 47 | "description" => "How much fat it has", 48 | }, 49 | { 50 | "name" => "organic", 51 | "type" => { 52 | "kind" => "SCALAR", 53 | "name" => "Boolean", 54 | }, 55 | "defaultValue" => "false", 56 | "description" => nil, 57 | }, 58 | { 59 | "name" => "order_by", 60 | "type" => { 61 | "kind" => "INPUT_OBJECT", 62 | "name" => "ResourceOrderType", 63 | }, 64 | "defaultValue" => "{direction: \"ASC\"}", 65 | "description" => nil, 66 | }, 67 | ], 68 | }, 69 | }, 70 | } 71 | result.should eq expected 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/graphql-crystal/introspection/introspection_query_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe "GraphQL::Introspection::INTROSPECTION_QUERY" do 4 | query_string = GraphQL::Schema::INTROSPECTION_QUERY 5 | result = Dummy::Schema.execute(query_string) 6 | 7 | it "runs" do 8 | result["data"].should be_truthy 9 | end 10 | 11 | pending "handles deeply nested (<= 7) schemas" do 12 | query_type = GraphQL::ObjectType.define do 13 | name "DeepQuery" 14 | field :foo do 15 | type !GraphQL::ListType.new( 16 | of_type: !GraphQL::ListType.new( 17 | of_type: !GraphQL::ListType.new( 18 | of_type: GraphQL::FLOAT_TYPE 19 | ) 20 | ) 21 | ) 22 | end 23 | end 24 | 25 | deep_schema = GraphQL::Schema.define do 26 | query query_type 27 | end 28 | 29 | result = deep_schema.execute(query_string) 30 | assert(GraphQL::Schema::Loader.load(result)) 31 | end 32 | 33 | pending "doesn't handle too deeply nested (< 8) schemas" do 34 | query_type = GraphQL::ObjectType.define do 35 | name "DeepQuery" 36 | field :foo do 37 | type !GraphQL::ListType.new( 38 | of_type: !GraphQL::ListType.new( 39 | of_type: !GraphQL::ListType.new( 40 | of_type: !GraphQL::ListType.new( 41 | of_type: GraphQL::FLOAT_TYPE 42 | ) 43 | ) 44 | ) 45 | ) 46 | end 47 | end 48 | 49 | deep_schema = GraphQL::Schema.define do 50 | query query_type 51 | end 52 | 53 | result = deep_schema.execute(query_string) 54 | assert_raises(KeyError) { 55 | GraphQL::Schema::Loader.load(result) 56 | } 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/graphql-crystal/introspection/schema_type_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | describe "GraphQL::Introspection::SchemaType" do 3 | query_string = %{ 4 | query getSchema { 5 | __schema { 6 | types { name } 7 | queryType { fields { name }} 8 | mutationType { fields { name }} 9 | } 10 | } 11 | } 12 | result = Dummy::Schema.execute(query_string) 13 | 14 | it "exposes the schema" do 15 | expected = {"data" => { 16 | "__schema" => { 17 | "types" => Dummy::Schema.types.values.map { |t| t.name.nil? ? (p t; raise("no name for #{t}")) : {"name" => t.name} }, 18 | "queryType" => { 19 | "fields" => [ 20 | {"name" => "allDairy"}, 21 | {"name" => "allEdible"}, 22 | {"name" => "cheese"}, 23 | {"name" => "cow"}, 24 | {"name" => "dairy"}, 25 | {"name" => "deepNonNull"}, 26 | {"name" => "error"}, 27 | {"name" => "executionError"}, 28 | {"name" => "favoriteEdible"}, 29 | {"name" => "fromSource"}, 30 | {"name" => "maybeNull"}, 31 | {"name" => "milk"}, 32 | {"name" => "root"}, 33 | {"name" => "searchDairy"}, 34 | {"name" => "valueWithExecutionError"}, 35 | ], 36 | }, 37 | "mutationType" => { 38 | "fields" => [ 39 | {"name" => "pushValue"}, 40 | {"name" => "replaceValues"}, 41 | ], 42 | }, 43 | }, 44 | }} 45 | result.should eq expected 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/graphql-crystal/introspection/type_type_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | pending "GraphQL::Introspection::TypeType" do 4 | query_string = <<-query_string 5 | query introspectionQuery { 6 | cheeseType: __type(name: "Cheese") { name, kind, fields { name, isDeprecated, type { kind, name, ofType { name } } } } 7 | milkType: __type(name: "Milk") { interfaces { name }, fields { type { kind, name, ofType { name } } } } 8 | dairyAnimal: __type(name: "DairyAnimal") { name, kind, enumValues(includeDeprecated: false) { name, isDeprecated } } 9 | dairyProduct: __type(name: "DairyProduct") { name, kind, possibleTypes { name } } 10 | animalProduct: __type(name: "AnimalProduct") { name, kind, possibleTypes { name }, fields { name } } 11 | missingType: __type(name: "NotAType") { name } 12 | } 13 | query_string 14 | result = Dummy::Schema.execute(query_string) 15 | cheese_fields = [ 16 | {"name" => "deeplyNullableCheese", "isDeprecated" => false, "type" => {"kind" => "OBJECT", "name" => "Cheese", "ofType" => nil}}, 17 | {"name" => "flavor", "isDeprecated" => false, "type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "String"}}}, 18 | {"name" => "id", "isDeprecated" => false, "type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "Int"}}}, 19 | {"name" => "nullableCheese", "isDeprecated" => false, "type" => {"kind" => "OBJECT", "name" => "Cheese", "ofType" => nil}}, 20 | {"name" => "origin", "isDeprecated" => false, "type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "String"}}}, 21 | {"name" => "selfAsEdible", "isDeprecated" => false, "type" => {"kind" => "INTERFACE", "name" => "Edible", "ofType" => nil}}, 22 | {"name" => "similarCheese", "isDeprecated" => false, "type" => {"kind" => "OBJECT", "name" => "Cheese", "ofType" => nil}}, 23 | {"name" => "source", "isDeprecated" => false, "type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "DairyAnimal"}}}, 24 | ] 25 | 26 | dairy_animals = [ 27 | {"name" => "COW", "isDeprecated" => false}, 28 | {"name" => "DONKEY", "isDeprecated" => false}, 29 | {"name" => "GOAT", "isDeprecated" => false}, 30 | {"name" => "REINDEER", "isDeprecated" => false}, 31 | {"name" => "SHEEP", "isDeprecated" => false}, 32 | ] 33 | 34 | it "exposes metadata about types" do 35 | expected = {"data" => { 36 | "cheeseType" => { 37 | "name" => "Cheese", 38 | "kind" => "OBJECT", 39 | "fields" => cheese_fields, 40 | }, 41 | "milkType" => { 42 | "interfaces" => [ 43 | {"name" => "Edible"}, 44 | {"name" => "AnimalProduct"}, 45 | {"name" => "LocalProduct"}, 46 | ], 47 | "fields" => [ 48 | {"type" => {"kind" => "LIST", "name" => nil, "ofType" => {"name" => "DairyProduct"}}}, 49 | {"type" => {"kind" => "SCALAR", "name" => "String", "ofType" => nil}}, 50 | {"type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "Float"}}}, 51 | {"type" => {"kind" => "LIST", "name" => nil, "ofType" => {"name" => "String"}}}, 52 | {"type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "ID"}}}, 53 | {"type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "String"}}}, 54 | {"type" => {"kind" => "INTERFACE", "name" => "Edible", "ofType" => nil}}, 55 | {"type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "DairyAnimal"}}}, 56 | ], 57 | }, 58 | "dairyAnimal" => { 59 | "name" => "DairyAnimal", 60 | "kind" => "ENUM", 61 | "enumValues" => dairy_animals, 62 | }, 63 | "dairyProduct" => { 64 | "name" => "DairyProduct", 65 | "kind" => "UNION", 66 | "possibleTypes" => [{"name" => "Milk"}, {"name" => "Cheese"}], 67 | }, 68 | "animalProduct" => { 69 | "name" => "AnimalProduct", 70 | "kind" => "INTERFACE", 71 | "possibleTypes" => [{"name" => "Cheese"}, {"name" => "Honey"}, {"name" => "Milk"}], 72 | "fields" => [ 73 | {"name" => "source"}, 74 | ], 75 | }, 76 | "missingType" => nil, 77 | }} 78 | result.should eq expected 79 | end 80 | 81 | pending "deprecated fields" do 82 | query_string = <<-query_string 83 | query introspectionQuery { 84 | cheeseType: __type(name: "Cheese") { name, kind, fields(includeDeprecated: true) { name, isDeprecated, type { kind, name, ofType { name } } } } 85 | dairyAnimal: __type(name: "DairyAnimal") { name, kind, enumValues(includeDeprecated: true) { name, isDeprecated } } 86 | } 87 | query_string 88 | deprecated_fields = [ 89 | {"name" => "fatContent", "isDeprecated" => true, "type" => {"kind" => "NON_NULL", "name" => nil, "ofType" => {"name" => "Float"}}}, 90 | ] 91 | 92 | it "can expose deprecated fields" do 93 | new_cheese_fields = ([deprecated_fields] + cheese_fields).sort_by { |f| f["name"] } 94 | expected = {"data" => { 95 | "cheeseType" => { 96 | "name" => "Cheese", 97 | "kind" => "OBJECT", 98 | "fields" => new_cheese_fields, 99 | }, 100 | "dairyAnimal" => { 101 | "name" => "DairyAnimal", 102 | "kind" => "ENUM", 103 | "enumValues" => dairy_animals + [{"name" => "YAK", "isDeprecated" => true}], 104 | }, 105 | }} 106 | result.should eq expected 107 | end 108 | 109 | pending "input objects" do 110 | query_string = %{ 111 | query introspectionQuery { 112 | __type(name: "DairyProductInput") { name, description, kind, inputFields { name, type { kind, name }, defaultValue } } 113 | } 114 | } 115 | 116 | it "exposes metadata about input objects" do 117 | expected = {"data" => { 118 | "__type" => { 119 | "name" => "DairyProductInput", 120 | "description" => "Properties for finding a dairy product", 121 | "kind" => "INPUT_OBJECT", 122 | "inputFields" => [ 123 | {"name" => "source", "type" => {"kind" => "NON_NULL", "name" => nil}, "defaultValue" => nil}, 124 | {"name" => "originDairy", "type" => {"kind" => "SCALAR", "name" => "String"}, "defaultValue" => "\"Sugar Hollow Dairy\""}, 125 | {"name" => "fatContent", "type" => {"kind" => "SCALAR", "name" => "Float"}, "defaultValue" => "0.3"}, 126 | {"name" => "organic", "type" => {"kind" => "SCALAR", "name" => "Boolean"}, "defaultValue" => "false"}, 127 | {"name" => "order_by", "type" => {"kind" => "INPUT_OBJECT", "name" => "ResourceOrderType"}, "defaultValue" => "{direction:\"ASC\"}"}, 128 | ], 129 | }, 130 | }} 131 | result.should eq expected 132 | end 133 | 134 | it "includes Relay fields" do 135 | res = StarWars::Schema.execute <<-GRAPHQL 136 | { 137 | __schema { 138 | types { 139 | name 140 | fields { 141 | name 142 | args { name } 143 | } 144 | } 145 | } 146 | } 147 | GRAPHQL 148 | 149 | type_result = res["data"]["__schema"]["types"].find { |t| t["name"] == "Faction" } 150 | field_result = type_result["fields"].find { |f| f["name"] == "bases" } 151 | all_arg_names = ["first", "after", "last", "before", "nameIncludes"] 152 | returned_arg_names = field_result["args"].map { |a| a["name"] } 153 | returned_arg_names.should eq all_arg_names 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/graphql-crystal/language/generation_spec.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | require "../../spec_helper" 4 | 5 | class GraphQL::Language::Parser 6 | def self.parse(prog : String, options = NamedTuple.new) 7 | new(GraphQL::Language::Lexer.new).parse(prog).as(GraphQL::Language::Document) 8 | end 9 | end 10 | 11 | def clean_string(string) 12 | string.gsub(/^ /m, "") 13 | .gsub(/#[^\n]*\n/m, "\n") 14 | .gsub(/[\n\s]+/m, "\n").strip 15 | end 16 | 17 | describe GraphQL::Language::Generation do 18 | query_string = %{ 19 | query getStuff($someVar: Int = 1, $anotherVar: [String!], $skipNested: Boolean! = false) @skip(if: false) { 20 | myField: someField(someArg: $someVar, ok: 1.4) @skip(if: $anotherVar) @thing(or: "Whatever") 21 | anotherField(someArg: [1, 2, 3]) { 22 | nestedField 23 | ...moreNestedFields @skip(if: $skipNested) 24 | } 25 | ... on OtherType @include(unless: false) { 26 | field(arg: [{key: "value", anotherKey: 0.9, anotherAnotherKey: WHATEVER}]) 27 | anotherField 28 | } 29 | ... { 30 | id 31 | } 32 | } 33 | 34 | fragment moreNestedFields on NestedType @or(something: "ok") { 35 | anotherNestedField 36 | } 37 | } 38 | 39 | document = GraphQL::Language::Parser.parse(query_string) 40 | describe ".generate" do 41 | it "should work" do 42 | document = GraphQL::Language::Parser.parse query_string 43 | document.to_query_string.gsub(/\s+/, " ").strip.should eq query_string.gsub(/\s+/, " ").strip 44 | end 45 | 46 | it "generates query string" do 47 | document.to_query_string.gsub(/\s+/, " ").strip.should eq query_string.gsub(/\s+/, " ").strip 48 | end 49 | 50 | context "inputs" do 51 | query_string = <<-query 52 | query { 53 | field(null_value: null, null_in_array: [1, null, 3], int: 3, float: 4.7e-24, bool: false, string: "☀︎🏆\\n escaped \\" unicode ¶ /", enum: ENUM_NAME, array: [7, 8, 9], object: {a: [1, 2, 3], b: {c: "4"}}, unicode_bom: "\xef\xbb\xbfquery") 54 | } 55 | query 56 | document = GraphQL::Language::Parser.parse(query_string) 57 | 58 | it "generate" do 59 | document.to_query_string.gsub(/(\s+|\n)/, " ").should eq query_string.gsub(/(\s+|\n)/, " ").strip 60 | end 61 | end 62 | 63 | describe "schema" do 64 | describe "schema with convention names for root types" do 65 | query_string = <<-schema 66 | schema { 67 | query: Query 68 | mutation: Mutation 69 | subscription: Subscription 70 | } 71 | schema 72 | 73 | document = GraphQL::Language::Parser.parse(query_string) 74 | 75 | it "omits schema definition" do 76 | document.to_query_string.should_not eq /schema/ 77 | end 78 | end 79 | 80 | context "schema with custom query root name" do 81 | query_string = <<-schema 82 | schema { 83 | query: MyQuery 84 | mutation: Mutation 85 | subscription: Subscription 86 | } 87 | schema 88 | 89 | document = GraphQL::Language::Parser.parse(query_string) 90 | 91 | it "includes schema definition" do 92 | document.to_query_string.should eq query_string.gsub(/^ /m, "").strip 93 | end 94 | end 95 | 96 | describe "schema with custom mutation root name" do 97 | query_string = <<-schema 98 | schema { 99 | query: Query 100 | mutation: MyMutation 101 | subscription: Subscription 102 | } 103 | schema 104 | 105 | document = GraphQL::Language::Parser.parse(query_string) 106 | 107 | it "includes schema definition" do 108 | document.to_query_string.should eq query_string.gsub(/^ /m, "").strip 109 | end 110 | end 111 | 112 | context "schema with custom subscription root name" do 113 | query_string = <<-schema 114 | schema { 115 | query: Query 116 | mutation: Mutation 117 | subscription: MySubscription 118 | } 119 | schema 120 | 121 | document = GraphQL::Language::Parser.parse(query_string) 122 | 123 | it "includes schema definition" do 124 | document.to_query_string.should eq query_string.gsub(/^ /m, "").strip 125 | end 126 | end 127 | 128 | describe "full featured schema" do 129 | # From: https://github.com/graphql/graphql-js/blob/bc96406ab44453a120da25a0bd6e2b0237119ddf/src/language/__tests__/schema-kitchen-sink.graphql 130 | query_string = <<-schema 131 | schema { 132 | query: QueryType 133 | mutation: MutationType 134 | } 135 | 136 | # Union description 137 | union AnnotatedUnion @onUnion = A | B 138 | 139 | type Foo implements Bar { 140 | one: Type 141 | two(argument: InputType!): Type 142 | three(argument: InputType, other: String): Int 143 | four(argument: String = "string"): String 144 | five(argument: [String] = ["string", "string"]): String 145 | six(argument: InputType = {key: "value"}): Type 146 | } 147 | 148 | # Scalar description 149 | scalar CustomScalar 150 | 151 | type AnnotatedObject @onObject(arg: "value") { 152 | annotatedField(arg: Type = "default" @onArg): Type @onField 153 | } 154 | 155 | interface Bar { 156 | one: Type 157 | four(argument: String = "string"): String 158 | } 159 | 160 | # Enum description 161 | enum Site { 162 | # Enum value description 163 | DESKTOP 164 | MOBILE 165 | } 166 | 167 | interface AnnotatedInterface @onInterface { 168 | annotatedField(arg: Type @onArg): Type @onField 169 | } 170 | 171 | union Feed = Story | Article | Advert 172 | 173 | # Input description 174 | input InputType { 175 | key: String! 176 | answer: Int = 42 177 | } 178 | 179 | union AnnotatedUnion @onUnion = A | B 180 | 181 | scalar CustomScalar 182 | 183 | # Directive description 184 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 185 | 186 | scalar AnnotatedScalar @onScalar 187 | 188 | enum Site { 189 | DESKTOP 190 | MOBILE 191 | } 192 | 193 | enum AnnotatedEnum @onEnum { 194 | ANNOTATED_VALUE @onEnumValue 195 | OTHER_VALUE 196 | } 197 | 198 | input InputType { 199 | key: String! 200 | answer: Int = 42 201 | } 202 | 203 | input AnnotatedInput @onInputObjectType { 204 | annotatedField: Type @onField 205 | } 206 | 207 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 208 | 209 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 210 | schema 211 | 212 | document = GraphQL::Language::Parser.parse(query_string) 213 | 214 | it "generate" do 215 | clean_string( 216 | document.to_query_string 217 | ).should eq clean_string( 218 | query_string 219 | ) 220 | end 221 | 222 | it "generate argument default to null" do 223 | query_string = <<-schema 224 | type Foo { 225 | one(argument: String = null): Type 226 | two(argument: Color = Red): Type 227 | } 228 | schema 229 | 230 | expected = <<-schema 231 | type Foo { 232 | one(argument: String): Type 233 | two(argument: Color = Red): Type 234 | } 235 | schema 236 | 237 | document = GraphQL::Language::Parser.parse(query_string) 238 | 239 | clean_string( 240 | document.to_query_string 241 | ).should eq clean_string( 242 | expected 243 | ) 244 | end 245 | 246 | it "doesn't mutate the document" do 247 | document.to_query_string.should eq document.to_query_string 248 | end 249 | end 250 | end 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /spec/graphql-crystal/language/lexer_spec.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # frozen_string_literal: true 3 | require "../../spec_helper" 4 | 5 | describe GraphQL::Language::Lexer do 6 | subject = GraphQL::Language::Lexer 7 | 8 | describe ".lex" do 9 | query_string = " \ 10 | { \ 11 | query getCheese { \ 12 | cheese(id: 1) { \ 13 | ... cheeseFields \ 14 | } \ 15 | } \ 16 | } \ 17 | " 18 | 19 | tokens = subject.lex(query_string) 20 | 21 | it "makes utf-8 comments" do 22 | comment_token = subject.lex("# 不要!\n{") 23 | comment_token.value.should eq "不要!" 24 | end 25 | 26 | it "unescapes escaped characters" do 27 | subject.lex( 28 | %{"\\" \\\\ \\/ \\b \\f \\n \\r \\t"} 29 | ).value.should eq "\" \\ / \b \f \n \r \t" 30 | end 31 | 32 | it "unescapes escaped unicode characters" do 33 | subject.lex(%{"\u0009"}).value.should eq "\t" 34 | end 35 | 36 | it "rejects bad unicode, even when there's good unicode in the string" do 37 | subject.lex(%{"\\u0XXF \\u0009"}) 38 | true.should eq false 39 | rescue 40 | true.should eq true 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/graphql-crystal/language/parser_spec.cr: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "../../spec_helper" 3 | 4 | class GraphQL::Language::Parser 5 | def self.parse(prog : String, options = {lookahead: false}) 6 | GraphQL::Language::Parser.new(GraphQL::Language::Lexer.new).parse(prog).as(GraphQL::Language::Document) 7 | end 8 | end 9 | 10 | describe GraphQL::Language::Parser do 11 | subject = GraphQL::Language::Parser 12 | 13 | describe "anonymous fragment extension" do 14 | query_strings = [ 15 | %{ 16 | fragment on NestedType @or(something: "ok") { 17 | anotherNestedField 18 | } 19 | }, 20 | %{ 21 | query getSchema { 22 | __schema { 23 | types { name } 24 | queryType { fields { name }} 25 | mutationType { fields { name }} 26 | } 27 | } 28 | }, 29 | %{ 30 | query HeroNameAndFriends($episode: Episode) { 31 | hero(episode: $episode) { 32 | name 33 | friends { 34 | name 35 | } 36 | } 37 | } 38 | }, 39 | %{ 40 | query Hero($episode: Episode, $withFriends: Boolean!) { 41 | hero(episode: $episode) { 42 | name 43 | friends @include(if: $withFriends) { 44 | name 45 | } 46 | } 47 | } 48 | }, 49 | %{ 50 | mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { 51 | createReview(episode: $ep, review: $review) { 52 | stars 53 | commentary 54 | } 55 | } 56 | }, 57 | %{query HeroForEpisode($ep: Episode!) { 58 | hero(episode: $ep) { 59 | name 60 | ... on Droid { 61 | primaryFunction 62 | } 63 | ... on Human { 64 | height 65 | } 66 | } 67 | } 68 | }, 69 | %{ 70 | query HeroForEpisode($ep: Episode!) { 71 | hero(episode: $ep) { 72 | name 73 | ... on Droid { 74 | primaryFunction 75 | } 76 | ... on Human { 77 | height 78 | } 79 | } 80 | } 81 | }, 82 | %{ 83 | { 84 | search(text: "an") { 85 | __typename 86 | ... on Human { 87 | name 88 | } 89 | ... on Droid { 90 | name 91 | } 92 | ... on Starship { 93 | name 94 | } 95 | } 96 | } 97 | }, 98 | %{ 99 | type Starship { 100 | id: ID! 101 | name: String! 102 | length(unit: LengthUnit = METER): Float 103 | } 104 | }, 105 | %{ 106 | # a spaceship 107 | # that flies through 108 | # space 109 | type Starship { 110 | id: ID! 111 | # the name 112 | # of that starship 113 | name: String! 114 | length( 115 | # the desired unit 116 | unit: LengthUnit = METER 117 | ): Float 118 | } 119 | }, 120 | ] 121 | 122 | query_strings.each do |query_string| 123 | it "parses different graphql docs: #{query_string}" do 124 | document = subject.parse(query_string) 125 | (document.definitions.size > 0).should eq true 126 | end 127 | end 128 | 129 | pending "parses the Dummy Schema" do 130 | document = subject.parse g 131 | end 132 | 133 | it "creates an anonymous fragment definition" do 134 | document = subject.parse query_strings[0] 135 | 136 | fragment = document.definitions.first 137 | fragment.is_a?(GraphQL::Language::FragmentDefinition).should eq true 138 | 139 | if (fragment.is_a?(GraphQL::Language::FragmentDefinition)) 140 | fragment.name.should eq nil 141 | fragment.selections.size.should eq 1 142 | fragment.type.as(GraphQL::Language::TypeName).name.should eq "NestedType" 143 | fragment.directives.size.should eq 1 144 | # fragment.position.should eq [2, 7] 145 | end 146 | end 147 | end 148 | 149 | it "parses empty arguments" do 150 | strings = [ 151 | "{ field { inner } }", 152 | ] 153 | 154 | strings.each do |query_str| 155 | doc = subject.parse(query_str) 156 | field = doc.definitions 157 | .first.as(GraphQL::Language::OperationDefinition) 158 | .selections.first.as(GraphQL::Language::Field) 159 | field.arguments.size.should eq 0 160 | field.selections.size.should eq 1 161 | end 162 | end 163 | 164 | pending "parses the test schema" do 165 | schema = Dummy::Schema 166 | schema_string = GraphQL::Schema::Printer.print_schema(schema) 167 | document = subject.parse(schema_string) 168 | assert_equal schema_string, document.to_query_string 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /spec/graphql-crystal/schema/custom_context_spec.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "../../spec_helper" 3 | 4 | describe GraphQL::Schema do 5 | File.open(LogStore::TEMPFILENAME, "w").truncate 6 | 7 | describe "user unauthenticated (via context)" do 8 | context = CustomContext.new({authenticated: false, name: "Anon"}, CUSTOM_CONTEXT_SCHEMA, nil) 9 | 10 | describe "query" do 11 | it "disallows viewing the logs" do 12 | expected = { 13 | "data" => { 14 | "logs" => nil, 15 | }, 16 | "errors" => [ 17 | { 18 | "message" => "you are not allowed to read the logs Anon!", 19 | "path" => ["logs"], 20 | }, 21 | ], 22 | } 23 | CUSTOM_CONTEXT_SCHEMA.execute( 24 | "{ logs { time, hostName, userName, message, process { name pid } } }", 25 | nil, 26 | nil, 27 | context 28 | ).should eq expected 29 | end 30 | end 31 | 32 | describe "mutation" do 33 | expected = { 34 | "data" => { 35 | "log" => nil, 36 | }, 37 | "errors" => [ 38 | { 39 | "message" => "you are not allowed to read the logs Anon!", 40 | "path" => ["log"], 41 | }, 42 | ], 43 | } 44 | 45 | mutation_string = %{ 46 | mutation log($payload: LogInput) { 47 | log(log: $payload) { 48 | time 49 | hostName 50 | userName 51 | message 52 | process { 53 | name 54 | pid 55 | } 56 | } 57 | } 58 | } 59 | 60 | mutation_args = { 61 | "payload" => { 62 | "time" => "Sep 4 22:57:21", 63 | "hostName" => "localhost", 64 | "message" => "something occured that need to be logged", 65 | "process" => { 66 | "name" => "crystal_graphql_server", 67 | "pid" => 1, 68 | }, 69 | }, 70 | } 71 | 72 | CUSTOM_CONTEXT_SCHEMA.execute(mutation_string, mutation_args, nil, context).should eq expected 73 | end 74 | end 75 | 76 | describe "user authenticated (via context)" do 77 | context = CustomContext.new({authenticated: true, name: "Alice"}, CUSTOM_CONTEXT_SCHEMA, nil) 78 | describe "query" do 79 | describe "logs query" do 80 | it "starts with an empty array of logs" do 81 | expected = { 82 | "data" => { 83 | "logs" => [] of JSON::Any, 84 | }, 85 | } 86 | CUSTOM_CONTEXT_SCHEMA.execute( 87 | "{ logs { time, hostName, userName, message, process { name pid } } }", 88 | nil, 89 | nil, 90 | context 91 | ).should eq expected 92 | end 93 | end 94 | end 95 | 96 | describe "mutation" do 97 | it "lets a log be pushed" do 98 | mutation_string = %{ 99 | mutation log($payload: LogInput) { 100 | log(log: $payload) { 101 | time 102 | hostName 103 | userName 104 | message 105 | process { 106 | name 107 | pid 108 | } 109 | } 110 | } 111 | } 112 | 113 | mutation_args = { 114 | "payload" => { 115 | "time" => "Sep 4 22:57:21", 116 | "hostName" => "localhost", 117 | "message" => "something occured that need to be logged", 118 | "process" => { 119 | "name" => "crystal_graphql_server", 120 | "pid" => 1, 121 | }, 122 | }, 123 | } 124 | 125 | expected = { 126 | "data" => { 127 | "log" => { 128 | "time" => "Sep 4 22:57:21", 129 | "hostName" => "localhost", 130 | "userName" => "Alice", 131 | "message" => "something occured that need to be logged", 132 | "process" => { 133 | "name" => "crystal_graphql_server", 134 | "pid" => 1, 135 | }, 136 | }, 137 | }, 138 | } 139 | 140 | CUSTOM_CONTEXT_SCHEMA.execute(mutation_string, mutation_args, nil, context).should eq expected 141 | end 142 | 143 | it "persists the created log so later queries show it" do 144 | expected = { 145 | "data" => { 146 | "logs" => [{ 147 | "time" => "Sep 4 22:57:21", 148 | "hostName" => "localhost", 149 | "userName" => "Alice", 150 | "message" => "something occured that need to be logged", 151 | "process" => { 152 | "name" => "crystal_graphql_server", 153 | "pid" => 1, 154 | }, 155 | }], 156 | }, 157 | } 158 | CUSTOM_CONTEXT_SCHEMA.execute( 159 | "{ logs { time, hostName, userName, message, process { name pid } } }", 160 | nil, 161 | nil, 162 | context 163 | ).should eq expected 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/graphql-crystal/schema/variables_spec.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "../../spec_helper" 3 | 4 | describe GraphQL::Schema, "using variables" do 5 | 6 | # When used in conjunction with Kemal as web framework, query variables 7 | # exposed by Kemal are of type Hash(String, JSON::Any). These should be able 8 | # to be parsed correctly. 9 | it "accepts query variables as Hash(String, JSON::Any)" do 10 | context = CustomContext.new({authenticated: true, name: "Anon"}, CUSTOM_CONTEXT_SCHEMA, nil) 11 | 12 | mutation = %{ 13 | mutation addLog($input: LogInput!) { 14 | log(log: $input) { 15 | time 16 | } 17 | } 18 | } 19 | json_any = JSON.parse({ time: "now", 20 | hostName: "docker host", 21 | process: { 22 | name: "crystal spec", 23 | pid: 42 24 | }, 25 | message: "in a bottle" }.to_json) 26 | variables = { "input" => json_any } 27 | variables.class.should eq Hash(String, JSON::Any) 28 | 29 | CUSTOM_CONTEXT_SCHEMA.execute(mutation, params: variables, context: context).should eq( 30 | { "data" => { "log" => { "time" => "now" } } } 31 | ) 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /spec/graphql-crystal/schema_spec.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "../spec_helper" 3 | 4 | describe GraphQL::Schema do 5 | describe "resolve" do 6 | it "answers a simple field request" do 7 | TestSchema::Schema.execute("{ user(id: 0) { name } }").should eq({"data" => {"user" => {"name" => "otto neverthere"}}}) 8 | end 9 | 10 | it "answers a simple field request for a field defined later in the inheritance chain (SpecialQuery)" do 11 | expected = { 12 | "data" => { 13 | "addresses" => [ 14 | {"city" => "London"}, 15 | {"city" => "Miami"}, 16 | {"city" => "CABA"}, 17 | ], 18 | }, 19 | } 20 | TestSchema::Schema.execute("{ addresses { city } }").should eq expected 21 | end 22 | 23 | it "answers a simple field request for a field defined later in the inheritance chain (SpecialQuery)" do 24 | expected = { 25 | "data" => { 26 | "addresses" => [ 27 | {"city" => "London", "street" => "Downing Street", "number" => 11}, 28 | {"city" => "Miami", "street" => "Sunset Boulevard", "number" => 114}, 29 | ], 30 | }, 31 | } 32 | TestSchema::Schema.execute(%{ 33 | { addresses(city: [London, Miami]) { city street number } } 34 | }).should eq expected 35 | end 36 | 37 | it "answers a simple field request for a field defined later in the inheritance chain (SpecialQuery)" do 38 | expected = { 39 | "data" => { 40 | "addresses" => [] of Nil, 41 | }, 42 | } 43 | TestSchema::Schema.execute(%{ 44 | { addresses(city: [Istanbul]) { city street number } } 45 | }).should eq expected 46 | end 47 | 48 | it "answers a simple field request for several fields" do 49 | expected = { 50 | "data" => { 51 | "user" => { 52 | "id" => 0, "name" => "otto neverthere", 53 | }, 54 | }, 55 | } 56 | TestSchema::Schema.execute( 57 | "{ user(id: 0) { id, name } }" 58 | ).should eq(expected) 59 | end 60 | 61 | it "answers a simple field request for a nested resource" do 62 | TestSchema::Schema.execute( 63 | "{ user(id: 0) { id, address { city } } }" 64 | ).should eq({ 65 | "data" => { 66 | "user" => { 67 | "id" => 0, 68 | "address" => { 69 | "city" => "London", 70 | }, 71 | }, 72 | }, 73 | }) 74 | end 75 | 76 | it "answers a more deep request for a list resource" do 77 | TestSchema::Schema.execute( 78 | "{ user(id: 0) { id, friends { id, name } } }" 79 | ).should eq({ 80 | "data" => { 81 | "user" => { 82 | "id" => 0, 83 | "friends" => [ 84 | {"id" => 2, "name" => "wilma nunca"}, 85 | {"id" => 1, "name" => "jennifer nonone"}, 86 | ], 87 | }, 88 | }, 89 | }) 90 | end 91 | 92 | it "answers a request for a field with a custom resolve callback" do 93 | TestSchema::Schema.execute( 94 | "{ user(id: 0) { full_address } }" 95 | ).should eq({"data" => { 96 | "user" => { 97 | "full_address" => "otto neverthere\n---------------\n11 Downing Street\n3231 London", 98 | }, 99 | }}) 100 | end 101 | 102 | it "answers a request for aliased fields" do 103 | expected = { 104 | "data" => { 105 | "firstUser" => { 106 | "name" => "otto neverthere", 107 | }, 108 | "secondUser" => { 109 | "name" => "jennifer nonone", 110 | "aliased_name" => "jennifer nonone", 111 | }, 112 | }, 113 | } 114 | 115 | TestSchema::Schema.execute(%{ 116 | { 117 | firstUser: user(id: 0) { 118 | name 119 | } 120 | secondUser: user(id: 1) { 121 | name, 122 | aliased_name: name 123 | } 124 | } 125 | }).should eq(expected) 126 | end 127 | 128 | it "answers a request for aliased resolving a fragment definition fields" do 129 | expected = { 130 | "data" => { 131 | "firstUser" => { 132 | "id" => 0, 133 | "name" => "otto neverthere", 134 | }, 135 | "secondUser" => { 136 | "id" => 1, 137 | "name" => "jennifer nonone", 138 | }, 139 | }, 140 | } 141 | 142 | TestSchema::Schema.execute(%{ 143 | { 144 | firstUser: user(id: 0) { 145 | ... userFields 146 | }, 147 | secondUser: user(id: 1) { 148 | ... userFields 149 | } 150 | } 151 | fragment userFields on User { 152 | id, name 153 | } 154 | }).should eq(expected) 155 | end 156 | 157 | it "answers a request for aliased field resolving an inline fragment definition" do 158 | expected = { 159 | "data" => { 160 | "firstUser" => { 161 | "id" => 0, 162 | "name" => "otto neverthere", 163 | }, 164 | }, 165 | } 166 | 167 | TestSchema::Schema.execute(%{ 168 | { 169 | firstUser: user(id: 0) { 170 | ... on User { 171 | id, name 172 | } 173 | } 174 | } 175 | }).should eq(expected) 176 | end 177 | 178 | it "answers a request with nullable arg and arg ommited" do 179 | TestSchema::Schema.execute( 180 | "query getAddresses($city: [City]) { addresses(city: $city) { city } }" 181 | ).should eq({ 182 | "data" => { 183 | "addresses" => [ 184 | {"city" => "London"}, 185 | {"city" => "Miami"}, 186 | {"city" => "CABA"} 187 | ], 188 | } 189 | }) 190 | end 191 | 192 | it "answers a request with non-nullable arg and arg provided" do 193 | TestSchema::Schema.execute( 194 | "query getAddresses($city: [City]!) { addresses(city: $city) { city } }", { 195 | "city" => [ 196 | "London" 197 | ] 198 | } 199 | ).should eq({ 200 | "data" => { 201 | "addresses" => [ 202 | {"city" => "London"} 203 | ], 204 | } 205 | }) 206 | end 207 | 208 | it "answers a request with non-nullable arg and arg provided with JSON::Any type" do 209 | TestSchema::Schema.execute( 210 | "query getAddresses($city: [City]!) { addresses(city: $city) { city } }", JSON.parse({ 211 | "city" => [ 212 | "London" 213 | ] 214 | }.to_json) 215 | ).should eq({ 216 | "data" => { 217 | "addresses" => [ 218 | {"city" => "London"} 219 | ], 220 | } 221 | }) 222 | end 223 | 224 | it "raises if non-nullable args ommited" do 225 | TestSchema::Schema.execute( 226 | "query getAddresses($city: [City]!) { addresses(city: $city) { city } }" 227 | ).should eq({ 228 | "data" => nil, "errors" => [{"message" => "missing variable city", "path" => [] of String}] 229 | }) 230 | end 231 | 232 | it "raises if no inline fragment was defined for the type actually returned" do 233 | expected = { 234 | "data" => { 235 | "firstUser" => nil, 236 | }, 237 | "errors" => [ 238 | { 239 | "message" => "no selections found for this field! \ 240 | maybe you forgot to define an inline fragment for this type in a union?", 241 | "path" => ["firstUser"], 242 | }, 243 | ], 244 | } 245 | 246 | TestSchema::Schema.execute(%{ 247 | { 248 | firstUser: user(id: 0) { 249 | ... on Droid { 250 | id, name 251 | } 252 | } 253 | } 254 | }).should eq expected 255 | end 256 | 257 | it "raises an error when I try to use an undefined fragment" do 258 | expected = { 259 | "data" => nil, 260 | "errors" => [ 261 | { 262 | "message" => "fragment \"userFieldsNonExistent\" is undefined", 263 | "path" => [] of String, 264 | }, 265 | ], 266 | } 267 | 268 | TestSchema::Schema.execute(%{ 269 | { 270 | firstUser: user(id: 0) { 271 | ... userFieldsNonExistent 272 | } 273 | } 274 | fragment userFields on User { 275 | id, name 276 | } 277 | }).should eq expected 278 | end 279 | 280 | it "raises an error if we request a field that hast not been defined" do 281 | bad_query_string = %{ 282 | { 283 | car(name: "toyota") { 284 | id, year 285 | } 286 | } 287 | } 288 | 289 | expected = { 290 | "data" => { 291 | "car" => nil, 292 | }, 293 | "errors" => [ 294 | { 295 | "message" => "field not defined.", 296 | "path" => ["car"], 297 | }, 298 | ], 299 | } 300 | 301 | TestSchema::Schema.execute(bad_query_string).should eq expected 302 | end 303 | 304 | it "raises an error if we request a field with an argument that hasn't been defined" do 305 | bad_query_string = %{ 306 | { 307 | user(name: "henry") { 308 | id, name 309 | } 310 | } 311 | } 312 | 313 | expected = { 314 | "data" => { 315 | "user" => nil, 316 | }, 317 | "errors" => [ 318 | { 319 | "message" => "Unknown argument \"name\"", 320 | "path" => ["user"], 321 | }, 322 | ], 323 | } 324 | 325 | TestSchema::Schema.execute(bad_query_string).should eq expected 326 | end 327 | 328 | it "raises an error if we request a field with defined argument using a wrong type" do 329 | bad_query_string = %{ 330 | { 331 | user(id: ["henry"]) { 332 | id, name 333 | } 334 | } 335 | } 336 | 337 | expected = { 338 | "data" => { 339 | "user" => nil, 340 | }, 341 | "errors" => [ 342 | { 343 | "message" => %{argument "id" is expected to be of type: "ID!"}, 344 | "path" => ["user"], 345 | }, 346 | ], 347 | } 348 | 349 | TestSchema::Schema.execute(bad_query_string).should eq expected 350 | end 351 | end 352 | 353 | describe "operationName" do 354 | it "multiple operations with valid operationName" do 355 | TestSchema::Schema.execute( 356 | %{ 357 | query UserOne{ user(id: 0) { name } } 358 | query UserTwo{ user(id: 0) { name } } 359 | }, 360 | nil, 361 | "UserOne" 362 | ).should eq({"data" => {"user" => {"name" => "otto neverthere"}}}) 363 | end 364 | 365 | it "one operation ignore operationName" do 366 | TestSchema::Schema.execute( 367 | %{ 368 | query UserOne{ user(id: 0) { name } } 369 | }, 370 | nil, 371 | "ignored operationName" 372 | ).should eq({"data" => {"user" => {"name" => "otto neverthere"}}}) 373 | end 374 | 375 | it "multiple operations without operationName" do 376 | expected = { 377 | "errors" => [ 378 | { 379 | "message" => "Must provide a valid operation name if query contains multiple operations.", 380 | "path" => [] of String, 381 | }, 382 | ], 383 | } 384 | TestSchema::Schema.execute( 385 | %{ 386 | query UserOne{ user(id: 0) { name } } 387 | query UserTwo{ user(id: 0) { name } } 388 | } 389 | ).should eq expected 390 | end 391 | 392 | it "multiple operations with invalid operationName" do 393 | expected = { 394 | "errors" => [ 395 | { 396 | "message" => "Must provide a valid operation name if query contains multiple operations.", 397 | "path" => [] of String, 398 | }, 399 | ], 400 | } 401 | TestSchema::Schema.execute( 402 | %{ 403 | query UserOne{ user(id: 0) { name } } 404 | query UserTwo{ user(id: 0) { name } } 405 | }, 406 | nil, 407 | "invalid operationName" 408 | ).should eq expected 409 | end 410 | end 411 | end 412 | -------------------------------------------------------------------------------- /spec/graphql-crystal/support/go_graphql_test_schema_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe GO_GRAPHQL_TEST_SCHEMA do 4 | it "parses the complex query that was used by the go-graphql benchmark" do 5 | query = %{ 6 | query Example($size: Int) { 7 | A, 8 | b, 9 | x: c 10 | ...c 11 | f 12 | ...on DataType { 13 | Pic(size: $size) 14 | promise { 15 | A 16 | } 17 | } 18 | deep { 19 | A 20 | b 21 | c 22 | deeper { 23 | A 24 | b 25 | } 26 | } 27 | } 28 | fragment c on DataType { 29 | d 30 | e 31 | } 32 | } 33 | 34 | expected = JSON.parse( 35 | %{ 36 | { 37 | "data": { 38 | "A": "Apple", 39 | "b": "Banana", 40 | "x": "Cookie", 41 | "d": "Donut", 42 | "e": "Egg", 43 | "f": "Fish", 44 | "Pic": "Pic of size: 50", 45 | "promise": { 46 | "A": "Apple" 47 | }, 48 | "deep": { 49 | "A": "Already Been Done", 50 | "b": "Boring", 51 | "c": [ 52 | "Contrived", 53 | null, 54 | "Confusing" 55 | ], 56 | "deeper": [{ 57 | "A": "Already Been Done", 58 | "b": "Boring" 59 | },{ 60 | "A": "Already Been Done", 61 | "b": "Boring" 62 | }] 63 | } 64 | } 65 | } 66 | }).as_h 67 | result = GO_GRAPHQL_TEST_SCHEMA.execute(query, ({"size" => 50})) 68 | result.should eq expected 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/graphql-crystal/support/star_wars_schema_introspection_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe GraphQL::Schema::Schema do 4 | 5 | query_string = <<-query 6 | query IntrospectionTypeQuery { 7 | __schema { 8 | types { 9 | name 10 | } 11 | } 12 | } 13 | query 14 | 15 | expected = { 16 | "data" => { 17 | "__schema" => { 18 | "types" => [ 19 | { 20 | "name" => "QueryType", 21 | }, 22 | { 23 | "name" => "Episode", 24 | }, 25 | { 26 | "name" => "Character", 27 | }, 28 | { 29 | "name" => "String", 30 | }, 31 | { 32 | "name" => "Human", 33 | }, 34 | { 35 | "name" => "Droid", 36 | }, 37 | { 38 | "name" => "__Schema", 39 | }, 40 | { 41 | "name" => "__Type", 42 | }, 43 | { 44 | "name" => "__TypeKind", 45 | }, 46 | { 47 | "name" => "Boolean", 48 | }, 49 | { 50 | "name" => "__Field", 51 | }, 52 | { 53 | "name" => "__InputValue", 54 | }, 55 | { 56 | "name" => "__EnumValue", 57 | }, 58 | { 59 | "name" => "__Directive", 60 | }, 61 | # Not Implemented ATM 62 | # { 63 | # "name" => "__DirectiveLocation" 64 | # } 65 | ], 66 | }, 67 | }, 68 | }["data"]["__schema"]["types"] 69 | 70 | result = StarWars::Schema.execute(query_string)["data"].as(Hash)["__schema"].as(Hash)["types"].as(Array) 71 | missing = expected.reject { |element| result.includes? element } 72 | superfluous = result.reject { |element| expected.includes? element } 73 | 74 | empty = [] of Hash(String, String) 75 | 76 | it "Allows querying the schema for types" do 77 | missing.should eq empty 78 | end 79 | 80 | pending "it should only return scalar types actually used in the schema" do 81 | superfluous.should eq empty 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/graphql-crystal/validation/evaluation_depth.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module EvaluationDepthTest 4 | SCHEMA_STRING = <<-schema_string 5 | schema { 6 | query: QueryType 7 | } 8 | 9 | type QueryType { 10 | firstElement: ListElement 11 | } 12 | 13 | type ListElement { 14 | index: Int 15 | next: ListElement 16 | } 17 | schema_string 18 | 19 | class ListElement 20 | include GraphQL::ObjectType 21 | 22 | def initialize(@index = 0); end 23 | 24 | field :index { @index } 25 | field :next { self.class.new(@index + 1) } 26 | end 27 | 28 | module QueryType 29 | include GraphQL::ObjectType 30 | extend self 31 | field :firstElement { ListElement.new } 32 | end 33 | 34 | Schema = GraphQL::Schema.from_schema(SCHEMA_STRING) 35 | Schema.max_depth 5 36 | Schema.query_resolver = QueryType 37 | end 38 | 39 | describe GraphQL::Schema do 40 | describe "Execution Depth Constraint" do 41 | it "allows queries that don't surpass the set max depth for the schema" do 42 | EvaluationDepthTest::Schema.execute(%< { firstElement { index } } >).should eq ({ 43 | "data" => { 44 | "firstElement" => { 45 | "index" => 0, 46 | }, 47 | }, 48 | }) 49 | 50 | EvaluationDepthTest::Schema 51 | .execute(%< { firstElement { next { next { index } } } } >) 52 | .should eq ({ 53 | "data" => { 54 | "firstElement" => { 55 | "next" => { 56 | "next" => { 57 | "index" => 2, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }) 63 | EvaluationDepthTest::Schema 64 | .execute(%< { firstElement { next { next { next { index } } } } } >) 65 | .should eq ({ 66 | "data" => { 67 | "firstElement" => { 68 | "next" => { 69 | "next" => { 70 | "next" => { 71 | "index" => 3, 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }) 78 | end 79 | 80 | it "throws an error when the max execution depth is reached" do 81 | EvaluationDepthTest::Schema 82 | .execute(%< { firstElement { next { next { next { next { index } } } } } } >) 83 | .should eq ({ 84 | "data" => { 85 | "firstElement" => { 86 | "next" => { 87 | "next" => { 88 | "next" => { 89 | "next" => nil, 90 | }, 91 | }, 92 | }, 93 | }, 94 | }, 95 | "errors" => [ 96 | { 97 | "message" => "max execution depth reached", 98 | "path" => [ 99 | "firstElement", "next", "next", "next", "next", 100 | ], 101 | }, 102 | ], 103 | }) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/graphql-crystal/validation/type_validation.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | SCHEMA_STRING = <<-schema_string 3 | schema { 4 | query: QueryType 5 | } 6 | 7 | type QueryType { 8 | test_nonnull(arg: String!): String 9 | test_list(arg: [String]): String 10 | test_scalars(id: ID!, int: Int, float: Float, string: String, bool: Boolean) : String 11 | } 12 | 13 | enum TestEnum { 14 | ValueA 15 | ValueB 16 | ValueC 17 | } 18 | 19 | input TestInput { 20 | id: ID! 21 | title: String! 22 | body: String 23 | } 24 | 25 | union TestUnion = TestEnum | Int 26 | 27 | schema_string 28 | 29 | TEST_SCHEMA = GraphQL::Schema.from_schema SCHEMA_STRING 30 | 31 | VALUES = { 32 | id: 23, 33 | uuid: "10526f6b-76a3-43cf-bad3-a521954a568f", 34 | string: "Hallo", 35 | int: 32, 36 | float: 32.3, 37 | bool: true, 38 | bool2: false, 39 | list: ["Hallo", "Hey"], 40 | mixed_list: [23, "hey", 23.2], 41 | enum: GraphQL::Language::AEnum.new(name: "ValueA"), 42 | null: nil, 43 | input_object: {"id" => "10526f6b-76a3-43cf-bad3-a521954a568f", "title" => "my title", "body" => "a body for this"}, 44 | input_object_missing: {"title" => "my title", "body" => "a body for this"}, 45 | input_object_superfluos: { 46 | "id" => "10526f6b-76a3-43cf-bad3-a521954a568f", 47 | "title" => "my title", 48 | "subtitle" => "what remains to be said", 49 | "body" => "a body for this", 50 | }, 51 | } 52 | 53 | TYPE_VALIDATION = GraphQL::TypeValidation.new TEST_SCHEMA.types 54 | 55 | def reject_other_than(type, leave_out) 56 | leave_out = ( 57 | leave_out.is_a?(Array) ? leave_out : [leave_out] 58 | ).map { |name| VALUES[name] } 59 | VALUES.to_a.reject { |(_, val)| leave_out.includes?(val) } 60 | .each do |(_, val)| 61 | it "rejects '#{val.inspect}'" do 62 | TYPE_VALIDATION.accepts?(type, val).should eq false 63 | end 64 | end 65 | end 66 | 67 | describe GraphQL::TypeValidation do 68 | describe GraphQL::Language::EnumTypeDefinition do 69 | type = TEST_SCHEMA.types["TestEnum"] 70 | 71 | it "accepts a string representing a valid enum value" do 72 | TYPE_VALIDATION.accepts?(type, VALUES[:enum].name).should eq true 73 | end 74 | 75 | it "rejects a string representing an invalid enum value" do 76 | TYPE_VALIDATION.accepts?(type, GraphQL::Language::AEnum.new(name: "ValueD")).should eq false 77 | end 78 | 79 | reject_other_than(type, [:enum, :null]) 80 | end 81 | 82 | describe GraphQL::Language::UnionTypeDefinition do 83 | type = TEST_SCHEMA.types["TestUnion"] 84 | 85 | it "accepts a string representing a valid enum value" do 86 | TYPE_VALIDATION.accepts?(type, VALUES[:enum]).should eq true 87 | end 88 | 89 | it "accepts a string representing a valid enum value" do 90 | TYPE_VALIDATION.accepts?(type, VALUES[:int]).should eq true 91 | end 92 | 93 | reject_other_than(type, [:enum, :int, :id, :null]) 94 | end 95 | 96 | describe GraphQL::Language::NonNullType do 97 | type = TEST_SCHEMA.types["QueryType"].as(GraphQL::Language::ObjectTypeDefinition) 98 | .fields.find(&.name.==("test_nonnull")).not_nil!.arguments.first.type 99 | 100 | it "accepts a String (it's of_type & not null)" do 101 | TYPE_VALIDATION.accepts?(type, "Hello").should eq true 102 | end 103 | 104 | it "rejects nil" do 105 | TYPE_VALIDATION.accepts?(type, nil).should eq false 106 | end 107 | end 108 | 109 | describe GraphQL::Language::ListType do 110 | type = TEST_SCHEMA.types["QueryType"].as(GraphQL::Language::ObjectTypeDefinition) 111 | .fields.find(&.name.==("test_list")).not_nil!.arguments.first.type 112 | 113 | it "accepts an array of String" do 114 | TYPE_VALIDATION.accepts?(type, VALUES[:list]).should eq true 115 | end 116 | 117 | reject_other_than(type, [:list, :null]) 118 | end 119 | 120 | describe GraphQL::Language::ScalarTypeDefinition do 121 | types = TEST_SCHEMA.types["QueryType"].as(GraphQL::Language::ObjectTypeDefinition) 122 | .fields.find(&.name.==("test_scalars")).not_nil!.arguments.map &.type 123 | 124 | describe "ID" do 125 | type = types.first 126 | 127 | it "accepts a numeric id" do 128 | TYPE_VALIDATION.accepts?(type, VALUES[:id]).should eq true 129 | end 130 | 131 | it "accepts a uuid" do 132 | TYPE_VALIDATION.accepts?(type, VALUES[:uuid]).should eq true 133 | end 134 | 135 | reject_other_than(type, [:id, :uuid, :string, :int, :null]) 136 | end 137 | describe "Int" do 138 | type = types[1] 139 | 140 | it "accepts an integer" do 141 | TYPE_VALIDATION.accepts?(type, VALUES[:int]).should eq true 142 | end 143 | 144 | reject_other_than(type, [:id, :int, :null]) 145 | end 146 | 147 | describe "Float" do 148 | type = types[2] 149 | 150 | it "accepts a float" do 151 | TYPE_VALIDATION.accepts?(type, VALUES[:float]).should eq true 152 | end 153 | 154 | it "accepts an integer" do 155 | TYPE_VALIDATION.accepts?(type, VALUES[:int]).should eq true 156 | end 157 | 158 | reject_other_than(type, [:float, :id, :int, :null]) 159 | end 160 | 161 | describe "String" do 162 | type = types[3] 163 | 164 | it "accepts a string" do 165 | TYPE_VALIDATION.accepts?(type, VALUES[:string]).should eq true 166 | end 167 | 168 | reject_other_than(type, [:string, :uuid, :null]) 169 | end 170 | 171 | describe "Boolean" do 172 | type = types[4] 173 | 174 | it "accepts true" do 175 | TYPE_VALIDATION.accepts?(type, VALUES[:bool]).should eq true 176 | end 177 | 178 | it "accepts false" do 179 | TYPE_VALIDATION.accepts?(type, VALUES[:bool2]).should eq true 180 | end 181 | 182 | reject_other_than(type, [:bool, :bool2, :null]) 183 | end 184 | end 185 | 186 | describe GraphQL::Language::InputObjectTypeDefinition do 187 | type = TEST_SCHEMA.types["TestInput"] 188 | 189 | it "accepts a hash of the expected structure" do 190 | TYPE_VALIDATION.accepts?(type, VALUES[:input_object]).should eq true 191 | end 192 | 193 | reject_other_than(type, [:input_object, :null]) 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /spec/graphql-crystal_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | require "./graphql-crystal/**" 3 | 4 | describe GraphQL do 5 | # TODO: Write tests 6 | 7 | it "works" do 8 | # false.should eq(true) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/core_ext/*" 3 | require "../src/graphql-crystal" 4 | require "./support/**" 5 | -------------------------------------------------------------------------------- /spec/support/custom_context_schema.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | class CustomContext < GraphQL::Schema::Context 4 | def initialize(@user : {authenticated: Bool, name: String}, @schema, @max_depth); end 5 | 6 | def authenticated 7 | @user[:authenticated] 8 | end 9 | 10 | def username 11 | @user[:name] 12 | end 13 | end 14 | 15 | class ProcessType 16 | include GraphQL::ObjectType 17 | 18 | def initialize(@name : String, @pid : Int32); end 19 | 20 | JSON.mapping({name: String, pid: Int32}) 21 | field :name 22 | field :pid 23 | end 24 | 25 | class LogType 26 | include GraphQL::ObjectType 27 | 28 | def initialize( 29 | @time : String, @hostName : String, 30 | @userName : String, @process : ProcessType, 31 | @message : String 32 | ); end 33 | 34 | JSON.mapping( 35 | time: String, 36 | hostName: String, 37 | userName: String, 38 | process: ProcessType, 39 | message: String 40 | ) 41 | 42 | field :time 43 | field :userName 44 | field :hostName 45 | field :process 46 | field :message 47 | end 48 | 49 | # 50 | # Structs to hold input data 51 | # 52 | struct ProcessInput < GraphQL::Schema::InputType 53 | JSON.mapping( 54 | name: String, 55 | pid: Int32 56 | ) 57 | 58 | def to_process_type 59 | ProcessType.new(@name, @pid) 60 | end 61 | end 62 | 63 | struct LogInput < GraphQL::Schema::InputType 64 | JSON.mapping( 65 | time: String, 66 | hostName: String, 67 | process: ProcessInput, 68 | message: String 69 | ) 70 | 71 | def to_log_type(username) 72 | LogType.new(@time, @hostName, username, @process.to_process_type, @message) 73 | end 74 | end 75 | 76 | module LogStore 77 | extend self 78 | TEMPFILENAME = "./__logs.log" 79 | 80 | `touch #{TEMPFILENAME}` 81 | 82 | def read_logs 83 | raw_content = File.read(TEMPFILENAME) 84 | Array(LogType).from_json raw_content 85 | rescue 86 | [] of LogType 87 | end 88 | 89 | def write_logs(logs) 90 | File.write(TEMPFILENAME, logs.to_json) 91 | end 92 | end 93 | 94 | module QueryType 95 | include ::GraphQL::ObjectType 96 | extend self 97 | 98 | field :logs do |args, context| 99 | context = context.as(CustomContext) 100 | unless context.authenticated 101 | raise "you are not allowed to read the logs #{context.username}!" 102 | end 103 | LogStore.read_logs 104 | end 105 | end 106 | 107 | module MutationType 108 | include ::GraphQL::ObjectType 109 | extend self 110 | 111 | field :log do |args, context| 112 | context = context.as(CustomContext) 113 | 114 | unless context.authenticated 115 | raise "you are not allowed to read the logs #{context.username}!" 116 | end 117 | 118 | new_log = args["log"].as(LogInput).to_log_type(context.username) 119 | 120 | LogStore.write_logs LogStore.read_logs << new_log 121 | new_log 122 | end 123 | end 124 | 125 | CUSTOM_CONTEXT_SCHEMA = ::GraphQL::Schema.from_schema( 126 | %{ 127 | schema { 128 | query: QueryType, 129 | mutation: MutationType 130 | } 131 | 132 | type QueryType { 133 | logs: [LogType] 134 | } 135 | 136 | type MutationType { 137 | log(log: LogInput) : LogType 138 | } 139 | 140 | input LogInput { 141 | time: String! 142 | hostName: String! 143 | process: ProcessInput! 144 | message: String! 145 | } 146 | 147 | input ProcessInput { 148 | name: String! 149 | pid: ID 150 | } 151 | 152 | type LogType { 153 | time: String! 154 | userName: String! 155 | hostName: String!, 156 | process: ProcessType! 157 | message: String 158 | } 159 | 160 | type ProcessType { 161 | name: String!, 162 | pid: ID! 163 | } 164 | } 165 | ) 166 | 167 | CUSTOM_CONTEXT_SCHEMA.tap do |schema| 168 | schema.query_resolver = QueryType 169 | schema.mutation_resolver = MutationType 170 | # add Types to parse from respective 171 | # Json Input Types 172 | schema.add_input_type("LogInput", LogInput) 173 | schema.add_input_type("ProcessInput", ProcessInput) 174 | end 175 | -------------------------------------------------------------------------------- /spec/support/dummy/dummy_data.cr: -------------------------------------------------------------------------------- 1 | require "../../../src/graphql-crystal/types/object_type" 2 | 3 | module Dummy 4 | class Cheese 5 | include GraphQL::ObjectType 6 | getter :source 7 | 8 | def initialize( 9 | @id : Int32, @flavour : String, 10 | @origin : String, @fat_content : Float64, @source : String | Int32 11 | ); end 12 | end 13 | 14 | CHEESES = { 15 | 1 => Cheese.new(1, "Brie", "France", 0.19, 1), 16 | 2 => Cheese.new(2, "Gouda", "Netherlands", 0.3, 1), 17 | 3 => Cheese.new(3, "Manchego", "Spain", 0.065, "SHEEP"), 18 | } 19 | 20 | class Milk 21 | include GraphQL::ObjectType 22 | getter :source 23 | 24 | def initialize( 25 | @id : Int32, @fat_content : Float64, 26 | @origin : String, @source : Int32, @flavours : Array(String) 27 | ); end 28 | end 29 | 30 | MILKS = { 31 | 1 => Milk.new(1, 0.04, "Antiquity", 1, ["Natural", "Chocolate", "Strawberry"]), 32 | } 33 | 34 | module DAIRY 35 | include GraphQL::ObjectType 36 | extend self 37 | 38 | ID = 1 39 | CHEESE = CHEESES[1] 40 | MILKS = [MILKS[1]] 41 | end 42 | 43 | module COW 44 | extend GraphQL::ObjectType 45 | ID = 1 46 | NAME = "Billy" 47 | LAST_PRODUCED_DAIRY = MILKS[1] 48 | end 49 | 50 | module MAYBE_NULL 51 | extend GraphQL::ObjectType 52 | CHEESE = nil 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/support/dummy/dummy_schema.cr: -------------------------------------------------------------------------------- 1 | require "./dummy_data" 2 | require "./dummy_schema_string" 3 | require "../../../src/graphql-crystal/schema" # def fetchItem(type, data : Hash) 4 | # Proc(Hash(String, JSON::Type), JSON::Type).new do |args| 5 | # data.find(&.[0].to_s.==(args["id"].to_s)).not_nil![1].as(JSON::Type) 6 | # end 7 | # end 8 | 9 | module Dummy 10 | module DairyAppQuery 11 | include ::GraphQL::ObjectType 12 | extend self 13 | 14 | # field :cheese { |args| fetchItem(Cheese, CHEESES).call(args.as(Hash(String,JSON::Type))) } 15 | # field :milk { |args| fetchItem(Milk, MILKS).call(args.as(Hash(String,JSON::Type))) } 16 | 17 | field :dairy { DAIRY } 18 | field :favouriteEdible { MILKS[1] } 19 | field :cow { COW } 20 | field :searchDairy do |args| 21 | source = args["product"].as(Array).first.as(Hash)["source"] 22 | products = CHEESES.values + MILKS.values 23 | if source 24 | products.select &.source.==(source) 25 | else 26 | products.first 27 | end 28 | end 29 | field :allDairy do |args| 30 | result = CHEESES.values + MILKS.values 31 | end 32 | 33 | field :allEdible { CHEESES.values + MILKS.values } 34 | field :error { raise("This error was raised on purpose") } 35 | field :executionError { raise("I don't have a dedicated ExecutionErrorObject :(") } 36 | field :maybeNull { Dummy::MAYBE_NULL } 37 | field :deepNonNull { nil } 38 | end 39 | 40 | module DairyAppMutation 41 | include ::GraphQL::ObjectType 42 | extend self 43 | 44 | field :pushValue do |args| 45 | args["value"] 46 | end 47 | 48 | field :replaceValues do |args| 49 | CHEESES.values + MILKS.values 50 | end 51 | end 52 | 53 | Schema = GraphQL::Schema.from_schema(Dummy::SCHEMA_STRING) 54 | Schema.query_resolver = DairyAppQuery 55 | Schema.mutation_resolver = DairyAppMutation 56 | end 57 | -------------------------------------------------------------------------------- /spec/support/dummy/dummy_schema_string.cr: -------------------------------------------------------------------------------- 1 | module Dummy 2 | SCHEMA_STRING = <<-schema_string 3 | # something that comes from 4 | # somewhere 5 | interface LocalProduct { 6 | # Place the thing 7 | # comes from 8 | origin: String! 9 | } 10 | 11 | # something you can eat, yum 12 | interface Edible { 13 | # Percentage which is fat 14 | fatContent: Float! 15 | # Place the edible comes from 16 | origin: String! 17 | selfAsEdible: Edible 18 | } 19 | 20 | # comes from an animal, 21 | # no joke 22 | interface AnimalProduct { 23 | # Animal which produced 24 | # this product 25 | source: DairyAnimalEnum! 26 | } 27 | 28 | # Something you can drink 29 | union Beverage = Milk 30 | 31 | # An animal which can yield milk 32 | enum DairyAnimal { 33 | # Animal with black and white spots 34 | COW 35 | # Animal with fur 36 | DONKEY 37 | # Animal with horns 38 | GOAT 39 | # Animal with horns 40 | REINDEER 41 | # Animal with wool 42 | SHEEP 43 | # Animal with long hair 44 | YAK 45 | } 46 | 47 | # Cultured dairy product 48 | type Cheese implements Edible, AnimalProduct, LocalProduct { 49 | # Unique identifier 50 | id: Int! 51 | # Kind of Cheese 52 | flavor: String! 53 | # Place the cheese comes from 54 | origin: String! 55 | # Animal which produced the milk for this cheese 56 | source: DairyAnimal! 57 | # Cheeses like this one 58 | similarCheese: Cheese 59 | # Cheeses like this one 60 | nullableCheese: Cheese 61 | # Cheeses like this one" 62 | deeplyNullableCheese: Cheese @deprecated(reason: "no longer supported") 63 | # Percentage which is milkfat 64 | fatContent: Float! 65 | } 66 | 67 | # Dairy beverage 68 | type Milk implements Edible, AnimalProduct, LocalProduct { 69 | id: ID! 70 | # Animal which produced this milk 71 | source: DairyAnimal! 72 | # Place the milk comes from 73 | origin: String! 74 | # Chocolate, Strawberry, etc 75 | flavors(limit: Int): [String] 76 | executionError: String 77 | allDiary: [DairyProduct] 78 | } 79 | 80 | interface Sweetener { 81 | sweetness: Int 82 | } 83 | 84 | # Sweet, dehydrated bee barf 85 | type Honey implements Edible, AnimalProduct, Sweetener{ 86 | # What flower this honey came from" 87 | flowerType: String 88 | } 89 | # A farm where milk is harvested and cheese is produced 90 | type Diary { 91 | id: ID! 92 | cheese: Cheese 93 | milks: [Milk] 94 | } 95 | 96 | # An object whose fields return nil 97 | type MaybeNull { 98 | cheese: Cheese 99 | } 100 | 101 | # Kinds of food made from milk 102 | # union DairyProduct { 103 | # } 104 | 105 | # A farm where milk is harvested 106 | # and cheese is produced 107 | type Cow { 108 | id: ID! 109 | name: String 110 | last_produced_dairy: DairyProduct 111 | cantBeNullButIs: String! 112 | cantBeNullButRaisesExecutionError: String! 113 | } 114 | 115 | # Properties used to determine ordering 116 | input ResourceOrderType { 117 | # ASC or DESC 118 | direction: String! 119 | } 120 | 121 | # Properties for finding a dairy product 122 | input DairyProductInput { 123 | # Where it came from 124 | source: DairyAnimal! 125 | # Dairy which produced it 126 | originDairy: String = "Sugar Hollow Dairy" 127 | # How much fat it has 128 | fatContent: Float = 0.3 129 | organic: Boolean = false 130 | order_by: ResourceOrderType = { direction: "ASC" } 131 | } 132 | 133 | type DeepNonNull { 134 | nonNullInt(returning: Int): Int! 135 | deepNonNull: DeepNonNull! 136 | } 137 | 138 | type ReplaceValuesInput { 139 | values: [Int!]! 140 | } 141 | 142 | # Query root of the system 143 | type DairyAppQuery { 144 | allDairy(executionErrorAtIndex: Int): [DairyProductUnion] 145 | allEdible: [EdibleInterface]! 146 | cheese: Cheese 147 | cow: Cow 148 | dairy: Dairy 149 | # To test possibly-null fields 150 | deepNonNull: DeepNonNull! 151 | # Raise an error 152 | error: String 153 | executionError: String 154 | # my favourite food 155 | favoriteEdible: Edible 156 | # Cheese from Source 157 | fromSource(source: dairyAnimal = COW): [Cheese] 158 | maybeNull:, MaybeNull 159 | milk: Milk 160 | root: String 161 | # Find dairy products matching a description 162 | searchDairy(product: [DairyProductInput] = [{source: "SHEEP" }]): DairyProductUnion! 163 | valueWithExecutionError: Int! 164 | } 165 | 166 | # The root for mutations in this schema 167 | type DairyAppMutation { 168 | # Push a value onto a 169 | # global array :D 170 | pushValue(value: Int): [Int!]! 171 | # Replace the global 172 | # array with new values 173 | replaceValues(input: ReplaceValuesInput!): [Int!]! 174 | } 175 | 176 | subscription Subscription { 177 | test: String 178 | } 179 | 180 | schema { 181 | query: DairyAppQuery 182 | mutation: DairyAppMutation 183 | subscription: Subscription 184 | } 185 | schema_string 186 | end 187 | -------------------------------------------------------------------------------- /spec/support/go_graphql_test_schema.cr: -------------------------------------------------------------------------------- 1 | module DataType 2 | include ::GraphQL::ObjectType 3 | extend self 4 | field :A { "Apple" } 5 | field :b { "Banana" } 6 | field :c { "Cookie" } 7 | field :d { "Donut" } 8 | field :e { "Egg" } 9 | field :f { "Fish" } 10 | field :Pic { |args| "Pic of size: #{args["size"]? || 50}" } 11 | field :deep { DeepDataType } 12 | field :promise { DataType } 13 | end 14 | 15 | module DeepDataType 16 | include ::GraphQL::ObjectType 17 | extend self 18 | field :A { "Already Been Done" } 19 | field :b { "Boring" } 20 | field :c { ["Contrived", nil, "Confusing"] } 21 | field :deeper { [self, self] } 22 | end 23 | 24 | GO_GRAPHQL_TEST_SCHEMA = ::GraphQL::Schema.from_schema( 25 | %{ 26 | schema { 27 | query: DataType 28 | } 29 | 30 | type DataType { 31 | A: String 32 | b: String 33 | c: String 34 | d: String 35 | e: String 36 | f: String 37 | Pic(size: Int): String 38 | deep: DeepDataType 39 | promise: DataType 40 | } 41 | 42 | type DeepDataType { 43 | A: String 44 | b: String 45 | c: [String] 46 | deeper: [DeepDataType] 47 | } 48 | } 49 | ) 50 | GO_GRAPHQL_TEST_SCHEMA.query_resolver = DataType 51 | -------------------------------------------------------------------------------- /spec/support/star_wars/star_wars_data.cr: -------------------------------------------------------------------------------- 1 | module StarWars 2 | enum EpisodeEnum 3 | NEWHOPE = 4 4 | EMPIRE = 5 5 | JEDI = 6 6 | end 7 | 8 | class Character 9 | include GraphQL::ObjectType 10 | getter :id, :name, :friends, :appears_in 11 | 12 | def initialize(@id : String, @name : String, @friends : Array(String), 13 | @appears_in : Array(EpisodeEnum)); end 14 | end 15 | 16 | class Human < Character 17 | getter :home_planet 18 | 19 | def initialize(@id : String, @name : String, @friends : Array(String), 20 | @appears_in : Array(EpisodeEnum), @home_planet : String?); end 21 | end 22 | 23 | class Droid < Character 24 | getter :primary_function 25 | 26 | def initialize(@id : String, @name : String, @friends : Array(String), 27 | @appears_in : Array(EpisodeEnum), @primary_function : String?); end 28 | end 29 | 30 | Characters = begin 31 | luke = { 32 | type: "Human", 33 | id: "1000", 34 | name: "Luke Skywalker", 35 | friends: ["1002", "1003", "2000", "2001"], 36 | appears_in: [4, 5, 6], 37 | home_planet: "Tatooine", 38 | } 39 | 40 | vader = { 41 | type: "Human", 42 | id: "1001", 43 | name: "Darth Vader", 44 | friends: ["1004"], 45 | appears_in: [4, 5, 6], 46 | home_planet: "Tatooine", 47 | } 48 | 49 | han = { 50 | type: "Human", 51 | id: "1002", 52 | name: "Han Solo", 53 | friends: ["1000", "1003", "2001"], 54 | appears_in: [4, 5, 6], 55 | } 56 | 57 | leia = { 58 | type: "Human", 59 | id: "1003", 60 | name: "Leia Organa", 61 | friends: ["1000", "1002", "2000", "2001"], 62 | appears_in: [4, 5, 6], 63 | home_planet: "Alderaan", 64 | } 65 | 66 | tarkin = { 67 | type: "Human", 68 | id: "1004", 69 | name: "Wilhuff Tarkin", 70 | friends: ["1001"], 71 | appears_in: [4], 72 | } 73 | 74 | threepio = { 75 | type: "Droid", 76 | id: "2000", 77 | name: "C-3PO", 78 | friends: ["1000", "1002", "1003", "2001"], 79 | appears_in: [4, 5, 6], 80 | primary_function: "Protocol", 81 | } 82 | 83 | artoo = { 84 | type: "Droid", 85 | id: "2001", 86 | name: "R2-D2", 87 | friends: ["1000", "1002", "1003"], 88 | appears_in: [4, 5, 6], 89 | primary_function: "Astromech", 90 | } 91 | 92 | [luke, vader, han, leia, tarkin, threepio, artoo].map do |data| 93 | init_data = { 94 | data.values[1], 95 | data.values[2], 96 | data.values[3], 97 | data.values[4].map { |e| EpisodeEnum.from_value(e) }, 98 | data.values[5]?, 99 | } 100 | if data[:type] == "Human" 101 | Human.new(*init_data) 102 | else 103 | Droid.new(*init_data) 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/support/star_wars/star_wars_schema.cr: -------------------------------------------------------------------------------- 1 | require "./star_wars_data" 2 | 3 | module StarWars 4 | SCHEMA_DEFINITION = <<-schema_string 5 | schema { 6 | query: QueryType 7 | } 8 | 9 | # One of the Movies 10 | enum Episode { 11 | # Episode IV: A New Hope 12 | NEWHOPE 13 | # Episode V: The Empire Strikes Back 14 | EMPIRE 15 | # Episode VI: Return of the Jedi 16 | JEDI 17 | } 18 | 19 | type QueryType { 20 | # Get the main hero of an episode 21 | hero(episode: Episode): Character 22 | # Get Humans by Id 23 | humans(ids: [String]): [Human] 24 | # Get a Human by Id 25 | human(id: String!): Human 26 | # Get a Droid by Id 27 | droid(id: String!): Droid 28 | } 29 | 30 | # A Star Wars Character 31 | interface Character { 32 | # The id of the character 33 | id: String 34 | 35 | # The name of the character 36 | name: String 37 | 38 | # The friends of the character or 39 | # an empty list if the have none 40 | friends: [Character] 41 | # Which movies they appear in 42 | appearsIn: [Episode] 43 | # All secrets about their past 44 | secretBackstory: String 45 | } 46 | 47 | # A humanoid Star Wars Character 48 | type Human implements Character { 49 | # the home planet of the 50 | # human, or null if unknown 51 | homePlanet: String 52 | } 53 | 54 | # A robotic Star Wars Character 55 | type Droid implements Character { 56 | # The primary function of the droid 57 | primaryFunction: String 58 | } 59 | schema_string 60 | 61 | abstract class Character 62 | field :id 63 | field :name 64 | field :friends do 65 | Characters.select { |c| self.friends.includes? c.id } 66 | end 67 | 68 | field :appearsIn { self.appears_in } 69 | field :secretBackstory do 70 | raise "secretBackstory is secret." 71 | end 72 | end 73 | 74 | class Human 75 | field :homePlanet { self.home_planet } 76 | end 77 | 78 | class Droid 79 | field :primaryFunction { self.primary_function } 80 | end 81 | 82 | module QueryType 83 | include GraphQL::ObjectType 84 | extend self 85 | 86 | field :hero do |args| 87 | if (args["episode"]? == "EMPIRE") 88 | Characters.find(&.id.==("1000")) 89 | else 90 | Characters.find(&.id.==("2001")) 91 | end 92 | end 93 | 94 | field :humans do |args| 95 | args["ids"].as(Array).map { |i| Characters.find(&.id.==(i)) } 96 | end 97 | 98 | field :human do |args| 99 | Characters.select(&.is_a?(Human)).find(&.id.==(args["id"])) 100 | end 101 | 102 | field :droid do |args| 103 | Characters.select(&.is_a?(Droid)).find(&.id.==(args["id"])) 104 | end 105 | end 106 | 107 | Schema = GraphQL::Schema.from_schema(SCHEMA_DEFINITION) 108 | Schema.query_resolver = QueryType 109 | end 110 | -------------------------------------------------------------------------------- /spec/support/test_schema.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "../../src/graphql-crystal/schema" 3 | 4 | module TestSchema 5 | # 6 | # Model Logic 7 | # 8 | enum CityEnum 9 | London 10 | Miami 11 | CABA 12 | Istanbul 13 | end 14 | 15 | Addresses = [ 16 | {"Downing Street", 11, CityEnum::London, 3231}, 17 | {"Sunset Boulevard", 114, CityEnum::Miami, 123439}, 18 | {"Avenida Santa Fé", 3042, CityEnum::CABA, 12398}, 19 | ].map { |vars| Address.new *vars } 20 | 21 | Users = [ 22 | "otto neverthere", "jennifer nonone", "wilma nunca", 23 | ].map_with_index do |name, idx| 24 | User.new idx, name, Addresses[idx] 25 | end 26 | 27 | Users[2].friends = [Users[1], Users[0]] 28 | Users[1].friends = [Users[2], Users[0]] 29 | Users[0].friends = [Users[2], Users[1]] 30 | 31 | class Address 32 | include GraphQL::ObjectType 33 | getter :street, :number, :city, :postal_code 34 | 35 | def initialize( 36 | @street : String, @number : Int32, 37 | @city : CityEnum, @postal_code : Int32 38 | ) 39 | end 40 | 41 | field :street 42 | field :number 43 | field :city # { city.to_s } 44 | field :postal_code 45 | end 46 | 47 | class User 48 | include GraphQL::ObjectType 49 | getter :id, :name, :address 50 | property :friends 51 | 52 | def initialize( 53 | @id : Int32, @name : String, 54 | @address : Address, 55 | @friends = Array(User).new 56 | ) 57 | end 58 | 59 | field :id 60 | field :name 61 | field :address 62 | field :friends 63 | field :full_address do 64 | <<-address 65 | #{name} 66 | #{name.size.times.to_a.map { "-" }.join} 67 | #{address.number} #{address.street} 68 | #{address.postal_code} #{address.city} 69 | address 70 | end 71 | end 72 | 73 | # 74 | # Schema Definition 75 | # 76 | SCHEMA_DEFINITION = <<-graphql_schema 77 | # Welcome to GraphiQL 78 | # 79 | # GraphiQL is an in-browser tool for writing, validating, and 80 | # testing GraphQL queries. 81 | # 82 | # Type queries into this side of the screen, and you will see intelligent 83 | # typeaheads aware of the current GraphQL type schema and live syntax and 84 | # validation errors highlighted within the text. 85 | # 86 | # GraphQL queries typically start with a "{" character. Lines that starts 87 | # with a # are ignored. 88 | # 89 | # An example GraphQL query might look like: 90 | # Welcome to GraphiQL 91 | # 92 | # GraphiQL is an in-browser tool for writing, validating, and 93 | # testing GraphQL queries. 94 | # 95 | # Type queries into this side of the screen, and you will see intelligent 96 | # typeaheads aware of the current GraphQL type schema and live syntax and 97 | # validation errors highlighted within the text. 98 | # 99 | # GraphQL queries typically start with a "{" character. Lines that starts 100 | # with a # are ignored. 101 | # 102 | # An example GraphQL query might look like: 103 | # 104 | # { 105 | # field(arg: "value") { 106 | # subField 107 | # } 108 | # } 109 | # 110 | # Keyboard shortcuts: 111 | # 112 | # Run Query: Ctrl-Enter (or press the play button above) 113 | # 114 | # Auto Complete: Ctrl-Space (or just start typing) 115 | schema { 116 | query: QueryType, 117 | mutation: MutationType 118 | } 119 | 120 | type QueryType { 121 | # A user in the system. 122 | user(id: ID!): User 123 | addresses(city: [City]): [Address] 124 | } 125 | 126 | enum City { 127 | London 128 | Miami @deprecated(reason: "is not a capital") 129 | CABA 130 | Istanbul 131 | } 132 | 133 | type User { 134 | id: ID! 135 | name: String @deprecated(reason: "for no apparent Reason") 136 | address: Address 137 | friends: [User] 138 | full_address: String 139 | } 140 | 141 | type Address { 142 | street: String 143 | number: Int 144 | city: City 145 | postal_code: Int 146 | } 147 | graphql_schema 148 | 149 | module QueryType 150 | include GraphQL::ObjectType 151 | extend self 152 | 153 | field :user do |args| 154 | Users.find &.id.==(args["id"]) 155 | end 156 | 157 | field :addresses do |args| 158 | (cities = args["city"]?) ? Addresses.select do |address| 159 | cities.as(Array).includes? address.city.to_s 160 | end : Addresses 161 | end 162 | end 163 | 164 | # 165 | # instantiate the schema and add the RootQuery Resolver 166 | # 167 | Schema = GraphQL::Schema.from_schema(SCHEMA_DEFINITION) 168 | Schema.query_resolver = QueryType 169 | end 170 | -------------------------------------------------------------------------------- /src/core_ext/named_tuple.cr: -------------------------------------------------------------------------------- 1 | struct NamedTuple 2 | def merge(other : NamedTuple | Nil) 3 | if other.is_a? NamedTuple 4 | merge_implementation(other) 5 | else 6 | self 7 | end 8 | end 9 | 10 | private def merge_implementation(other : U) forall U 11 | {% begin %} 12 | NamedTuple.new( 13 | {% for key in (T.keys + U.keys).uniq %} 14 | {% if U.keys.includes?(key) %} 15 | {{key}}: other[:{{key}}], 16 | {% else %} 17 | {{key}}: self[:{{key}}], 18 | {% end %} 19 | {% end %} 20 | ) 21 | {% end %} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /src/core_ext/object.cr: -------------------------------------------------------------------------------- 1 | class Object 2 | def debug 3 | pp self 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /src/graphql-crystal.cr: -------------------------------------------------------------------------------- 1 | require "./core_ext/*" 2 | require "./graphql-crystal/*" 3 | 4 | module GraphQL 5 | # TODO Put your code here 6 | end 7 | -------------------------------------------------------------------------------- /src/graphql-crystal/directives.cr: -------------------------------------------------------------------------------- 1 | require "./directives/*" 2 | -------------------------------------------------------------------------------- /src/graphql-crystal/directives/deprecated_directive.cr: -------------------------------------------------------------------------------- 1 | require "./directive" 2 | 3 | module GraphQL 4 | module Directives 5 | # 6 | # Directive that allows to annote 7 | # fields as deprecated in the schema 8 | # definition 9 | # 10 | module IsDeprecated 11 | macro included 12 | @deprecated : Bool? 13 | @deprecation_reason : String? 14 | 15 | def _graphql_deprecated 16 | @deprecated ||= directives.any? &.name.==("deprecated") 17 | end 18 | 19 | def _graphql_deprecation_reason(schema) 20 | @deprecation_reason ||= ( 21 | if dir = directives.find(&.name.==("deprecated")) 22 | dir.arguments.find(&.name = "reason").try(&.value) || 23 | schema.directive_definitions["deprecated"] 24 | .arguments.find(&.name.==("reason")).try &.default_value 25 | else 26 | nil 27 | end.as(String?) 28 | ) 29 | end 30 | 31 | field :isDeprecated do 32 | _graphql_deprecated 33 | end 34 | 35 | field :deprecationReason do |args, context| 36 | _graphql_deprecation_reason(context.schema) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/graphql-crystal/directives/directive.cr: -------------------------------------------------------------------------------- 1 | require "../schema/middleware" 2 | 3 | module GraphQL 4 | # 5 | # A module to be included in a 6 | # directive to act as a middleware 7 | # 8 | module Directive 9 | include GraphQL::Schema::Middleware 10 | property args : Hash(String, ReturnType)? 11 | 12 | def call(*args) 13 | call_next(*args) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/graphql-crystal/directives/include_directive.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Directives 3 | # 4 | # The @include(if: ...) directive 5 | # 6 | class IncludeDirective 7 | include GraphQL::Directive 8 | getter :name 9 | @name = "include" 10 | 11 | def call(_field_definition, _selections, _resolved, _context) 12 | if args.try &.["if"] 13 | call_next(_field_definition, _selections, _resolved, _context) 14 | else 15 | {nil, [] of Error} 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/graphql-crystal/directives/skip_directive.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Directives 3 | # 4 | # The @skip(if: ...) directive 5 | # 6 | class SkipDirective 7 | include GraphQL::Directive 8 | getter :name 9 | @name = "skip" 10 | 11 | def call(_field_definition, _selections, _resolved, _context) 12 | if args.try &.["if"] 13 | {nil, [] of Error} 14 | else 15 | call_next(_field_definition, _selections, _resolved, _context) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/graphql-crystal/language.cr: -------------------------------------------------------------------------------- 1 | require "./language/lexer" 2 | require "./language/nodes" 3 | require "./language/parser" 4 | require "./language/generation" 5 | 6 | module GraphQL::Language 7 | # 8 | # Parse a query string and return the Document 9 | # 10 | def self.parse(query_string, options = NamedTuple.new) : GraphQL::Language::Document 11 | GraphQL::Language::Parser.new( 12 | GraphQL::Language::Lexer.new 13 | ).parse(query_string) 14 | rescue e 15 | raise e 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/graphql-crystal/language/ast.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Language 3 | abstract class ASTNode 4 | macro make_value_methods 5 | 6 | macro accessors(name, type, default) 7 | def \\{{name}} 8 | @\\{{name}} 9 | end 10 | 11 | def \\{{name}}=(@\\{{name}} : \\{{type}}); end 12 | end 13 | 14 | macro traverse(name, *values) 15 | \{% 16 | visit = VISITS.find(&.[0].==(name)) 17 | unless visit 18 | VISITS << {name, [] of Symbol} 19 | visit = VISITS.find(&.[0].==(name)) 20 | end 21 | values.map do |value| 22 | visit[1] = visit[1] + [value] 23 | end 24 | %} 25 | end 26 | 27 | macro values(args) 28 | \{% 29 | args.keys.map do |k| 30 | VALUES << { k, args[k] } 31 | end 32 | %} 33 | property \{{args.keys.map{|k| "#{k} : #{args[k]}"}.join(", ").id}} 34 | end 35 | 36 | macro inherited 37 | make_value_methods 38 | end 39 | end 40 | 41 | def self.values 42 | NamedTuple.new() 43 | end 44 | 45 | def values 46 | NamedTuple.new() 47 | end 48 | 49 | def ==(other) 50 | self.class == other.class 51 | end 52 | 53 | # def inspect 54 | # "#{self.class.name}(" + 55 | # if vs = values 56 | # vs.map do |k, v| 57 | # value = if v.is_a?(Array) 58 | # "[" + v.map{ |vv| vv.inspect.as(String) }.join(", ") + "]" 59 | # elsif v.is_a?(Hash) 60 | # "{" + v.map{ |kk, vv| "#{kk.inspect}: #{vv.inspect}".as(String) }.join(", ") + "}" 61 | # else 62 | # v.inspect 63 | # end 64 | # "#{k}: #{value}" 65 | # end.join(", ") 66 | # else 67 | # "" 68 | # end + ")" 69 | # end 70 | 71 | def_clone 72 | 73 | macro inherited 74 | 75 | make_value_methods 76 | 77 | macro finished 78 | def_clone 79 | 80 | def ==(other : \{{@type}}) 81 | if self.object_id == other.object_id 82 | true 83 | else 84 | \{{ (VALUES.map { |v| "(@#{v[0].id} == other.#{v[0].id})".id } + ["super(other)".id]).join(" && ").id }} 85 | end 86 | end 87 | 88 | def self.values 89 | super\{% if VALUES.size > 0 %}.merge NamedTuple.new(\{{ VALUES.map { |v| "#{v[0].id}: #{v[1].id}".id }.join(",").id }})\{% end %} 90 | end 91 | 92 | def values 93 | super\{% if VALUES.size > 0 %}.merge NamedTuple.new(\{{ VALUES.map { |v| "#{v[0].id}: @#{v[0]}".id }.join(",").id }})\{% end %} 94 | end 95 | 96 | \{% for tuple in VISITS %} 97 | \{% key = tuple[0]; elements = tuple[1]%} 98 | def map_\{{key.id}}(&block : ASTNode -> _) 99 | visited_ids = [] of UInt64 100 | visit(\{{key}}, visited_ids, block) 101 | end 102 | \{% end %} 103 | 104 | # Recursively apply the given block to each 105 | # node that gets visited with the given key 106 | # which nodes get traverses for a given key 107 | # can be set on a class via the: 108 | # `traverse :name, :child_1, :child2` 109 | # macro. If no children are defined for a 110 | # given traversal path name the block is invoked 111 | # only with self. 112 | def visit(name, visited_ids = [] of UInt64, block = Proc(ASTNode, ASTNode?).new {}) 113 | \{% if VISITS.size > 0 %} 114 | case name 115 | \{% for tuple in VISITS %}\ 116 | when \{{tuple[0]}} 117 | \{% for key in tuple[1]%} 118 | %val = \{{key.id}} 119 | if %val.is_a?(Array) 120 | %result = %val.map! do |v| 121 | next v if visited_ids.includes? v.object_id 122 | visited_ids << v.object_id 123 | res = v.visit(name, visited_ids, block) 124 | res.is_a?(ASTNode) ? res : v 125 | end 126 | else 127 | unless %val == nil || visited_ids.includes? %val.object_id 128 | visited_ids << %val.object_id 129 | %result = %val.not_nil!.visit(name, visited_ids, block) 130 | self.\{{key.id}}=(%result) 131 | end 132 | end 133 | \{% end %} 134 | \{% end %}\ 135 | end 136 | \{% end %}\ 137 | res = block.call(self) 138 | res.is_a?(self) ? res : self 139 | end 140 | 141 | \{% 142 | signatures = VALUES.map { |v| "#{v[0].id} " + (v[2] ? "= #{v[2]}" : "") } 143 | signature = (signatures + ["**rest"]).join(", ").id 144 | assignments = VALUES.map do |v| 145 | if v[1].id =~ /^Array/ 146 | type = v[1].id.gsub(/Array\(/, "").gsub(/\)/, "") 147 | "@#{v[0].id} = #{v[0].id}.as(Array).map(&.as(#{type})).as(#{v[1].id})" 148 | else 149 | "@#{v[0].id} = #{v[0].id}.as(#{v[1].id})" 150 | end 151 | end 152 | %} 153 | 154 | def initialize(\{{signature}}) 155 | \{{assignments.size > 0 ? assignments.join("\n").id : "".id}} 156 | super(**rest) 157 | end 158 | 159 | end 160 | end 161 | 162 | VALUES = [] of Tuple(Symbol, Object.class) 163 | VISITS = [] of Tuple(Symbol, Array(Symbol)) 164 | 165 | macro inherited 166 | VALUES = [] of Tuple(Symbol, Object.class) 167 | VISITS = [] of Tuple(Symbol, Array(Symbol)) 168 | end 169 | 170 | make_value_methods 171 | 172 | end 173 | end 174 | end -------------------------------------------------------------------------------- /src/graphql-crystal/language/generation.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module GraphQL 4 | module Language 5 | # Exposes {.generate}, which turns AST nodes back into query strings. 6 | # Turn an AST node back into a string. 7 | # 8 | # @example Turning a document into a query 9 | # document = GraphQL.parse(query_string) 10 | # Generation.generate(document) 11 | # # => "{ ... }" 12 | # 13 | # @param node [AbstractNode] an AST node to recursively stringify 14 | # @param indent [String] Whitespace to add to each printed node 15 | # @return [String] Valid GraphQL for `node` 16 | module Generation 17 | def self.generate(node : Document, indent : String = "") 18 | node.definitions.map { |d| generate(d) }.join("\n\n") 19 | end 20 | 21 | def self.generate(node : Argument, indent : String = "") 22 | "#{node.name}: #{generate(node.value)}" 23 | end 24 | 25 | def self.generate(node : Directive, indent : String = "") 26 | out = "@#{node.name}" 27 | out += "(#{node.arguments.map { |a| generate(a).as(String) }.join(", ")})" if node.arguments.any? 28 | out 29 | end 30 | 31 | def self.generate(node : AEnum, indent : String = "") 32 | "#{node.name}" 33 | end 34 | 35 | def self.generate(node : NullValue, indent : String = "") 36 | "null" 37 | end 38 | 39 | def self.generate(node : Field, indent : String = "") 40 | out = "" 41 | out += "#{node._alias}: " if node._alias 42 | out += "#{node.name}" 43 | out += "(#{node.arguments.map { |a| generate(a).as(String) }.join(", ")})" if node.arguments.any? 44 | out += generate_directives(node.directives) 45 | out += generate_selections(node.selections, indent: indent) 46 | out 47 | end 48 | 49 | def self.generate(node : FragmentDefinition, indent : String = "") 50 | out = "#{indent}fragment #{node.name}" 51 | if node.type 52 | out += " on #{generate(node.type)}" 53 | end 54 | out += generate_directives(node.directives) 55 | out += generate_selections(node.selections, indent: indent) 56 | out 57 | end 58 | 59 | def self.generate(node : FragmentSpread, indent : String = "") 60 | out = "#{indent}...#{node.name}" 61 | if node.directives.any? 62 | out += " " + node.directives.map { |d| generate(d).as(String) }.join(" ") 63 | end 64 | end 65 | 66 | def self.generate(node : InlineFragment, indent : String = "") 67 | out = "#{indent}..." 68 | if node.type 69 | out += " on #{generate(node.type)}" 70 | end 71 | out += generate_directives(node.directives) 72 | out += generate_selections(node.selections, indent: indent) 73 | out 74 | end 75 | 76 | def self.generate(node : InputObject, indent : String = "") 77 | generate(node.to_h) 78 | end 79 | 80 | def self.generate(node : ListType, indent : String = "") 81 | "[#{generate(node.of_type)}]" 82 | end 83 | 84 | def self.generate(node : NonNullType, indent : String = "") 85 | "#{generate(node.of_type)}!" 86 | end 87 | 88 | def self.generate(node : OperationDefinition, indent : String = "") 89 | out = "#{indent}#{node.operation_type}" 90 | if node.name 91 | out += " #{node.name}" 92 | end 93 | out += "(#{node.variables.map { |v| generate(v) }.join(", ")})" if node.variables.any? 94 | out += generate_directives(node.directives) 95 | out += generate_selections(node.selections, indent: indent) 96 | out 97 | end 98 | 99 | def self.generate(node : TypeName, indent : String = "") 100 | "#{node.name}" 101 | end 102 | 103 | def self.generate(node : VariableDefinition) 104 | out = "$#{node.name}: #{generate(node.type)}" 105 | unless node.default_value.nil? 106 | out += " = #{generate(node.default_value)}" 107 | end 108 | out 109 | end 110 | 111 | def self.generate(node : VariableIdentifier, indent : String = "") 112 | "$#{node.name}" 113 | end 114 | 115 | def self.generate(node : SchemaDefinition, indent : String = "") 116 | if (node.query.nil? || node.query == "Query") && 117 | (node.mutation.nil? || node.mutation == "Mutation") && 118 | (node.subscription.nil? || node.subscription == "Subscription") 119 | return "" 120 | end 121 | out = "schema {\n" 122 | out += " query: #{node.query}\n" if node.query 123 | out += " mutation: #{node.mutation}\n" if node.mutation 124 | out += " subscription: #{node.subscription}\n" if node.subscription 125 | out += "}" 126 | end 127 | 128 | def self.generate(node : ScalarTypeDefinition, indent : String = "") 129 | out = generate_description(node) 130 | out += "scalar #{node.name}" 131 | out += generate_directives(node.directives) 132 | end 133 | 134 | def self.generate(node : ObjectTypeDefinition, indent : String = "") 135 | out = generate_description(node) 136 | out += "type #{node.name}" 137 | out += generate_directives(node.directives) 138 | out += " implements " + node.interfaces.map { |i| i.as(String) }.join(", ") unless node.interfaces.empty? 139 | out += generate_field_definitions(node.fields) 140 | end 141 | 142 | def self.generate(node : InputValueDefinition, indent : String = "") 143 | out = "#{node.name}: #{generate(node.type)}" 144 | out += " = #{generate(node.default_value)}" unless node.default_value.nil? 145 | out += generate_directives(node.directives) 146 | end 147 | 148 | def self.generate(node : FieldDefinition, indent : String = "") 149 | out = node.name.dup 150 | unless node.arguments.empty? 151 | out += "(" + node.arguments.map { |arg| generate(arg).as(String) }.join(", ") + ")" 152 | end 153 | out += ": #{generate(node.type)}" 154 | out += generate_directives(node.directives) 155 | end 156 | 157 | def self.generate(node : InterfaceTypeDefinition, indent : String = "") 158 | out = generate_description(node) 159 | out += "interface #{node.name}" 160 | out += generate_directives(node.directives) 161 | out += generate_field_definitions(node.fields) 162 | end 163 | 164 | def self.generate(node : UnionTypeDefinition, indent : String = "") 165 | out = generate_description(node) 166 | out += "union #{node.name}" 167 | out += generate_directives(node.directives) 168 | out += " = " + node.types.map { |t| t.as(NameOnlyNode).name }.join(" | ") 169 | end 170 | 171 | def self.generate(node : EnumTypeDefinition, indent : String = "") 172 | out = generate_description(node) 173 | out += "enum #{node.name}#{generate_directives(node.directives)} {\n" 174 | node.fvalues.each_with_index do |value, i| 175 | out += generate_description(value, indent: " ", first_in_block: i == 0) 176 | out += generate(value) || "" 177 | end 178 | out += "}" 179 | end 180 | 181 | def self.generate(node : EnumValueDefinition, indent : String = "") 182 | out = " #{node.name}" 183 | out += generate_directives(node.directives) 184 | out += "\n" 185 | end 186 | 187 | def self.generate(node : InputObjectTypeDefinition, indent : String = "") 188 | out = generate_description(node) 189 | out += "input #{node.name}" 190 | out += generate_directives(node.directives) 191 | out += " {\n" 192 | node.fields.each.with_index do |field, i| 193 | out += generate_description(field, indent: " ", first_in_block: i == 0) 194 | out += " #{generate(field)}\n" 195 | end 196 | out += "}" 197 | end 198 | 199 | def self.generate(node : DirectiveDefinition, indent : String = "") 200 | out = generate_description(node) 201 | out += "directive @#{node.name}" 202 | out += "(#{node.arguments.map { |a| generate(a).as(String) }.join(", ")})" if node.arguments.any? 203 | out += " on #{node.locations.join(" | ")}" 204 | end 205 | 206 | # def self.generate(node : AbstractNode, indent : String = "") 207 | # node.to_query_string() 208 | # end 209 | 210 | def self.generate(node : Float | Int | String | Nil | Bool, indent : String = "") 211 | node.to_json 212 | end 213 | 214 | def self.generate(node : Symbol, indent : String = "") 215 | node.to_s.capitalize 216 | end 217 | 218 | def self.generate(node : Array, indent : String = "") 219 | "[#{node.map { |v| generate(v) }.join(", ")}]" 220 | end 221 | 222 | def self.generate(node : Hash, indent : String = "") 223 | value = node.map { |k, v| "#{k}: #{generate(v)}" }.join(", ") 224 | "{#{value}}" 225 | end 226 | 227 | def self.generate(node, indent : String = "") 228 | raise "TypeError (please define it :) )" 229 | "" 230 | end 231 | 232 | def self.generate_directives(directives, indent : String = "") 233 | if directives.any? 234 | directives.map { |d| " #{generate(d)}" }.join 235 | else 236 | "" 237 | end 238 | end 239 | 240 | def self.generate_selections(selections, indent : String = "") 241 | if selections.any? 242 | out = " {\n" 243 | selections.each do |selection| 244 | out += generate(selection, indent: indent + " ").to_s + "\n" 245 | end 246 | out += "#{indent}}" 247 | else 248 | "" 249 | end 250 | end 251 | 252 | def self.generate_description(node, indent = "", first_in_block = true) 253 | return "" unless node.description 254 | 255 | description = indent != "" && !first_in_block ? "\n" : "" 256 | description += "#{indent}# #{node.description}\n" 257 | end 258 | 259 | def self.generate_field_definitions(fields, indent : String = "") 260 | out = " {\n" 261 | fields.each.with_index do |field, i| 262 | out += generate_description(field, indent: " ", first_in_block: i == 0) 263 | out += " #{generate(field)}\n" 264 | end 265 | out += "}" 266 | end 267 | end 268 | end 269 | end 270 | -------------------------------------------------------------------------------- /src/graphql-crystal/language/lexer.cr: -------------------------------------------------------------------------------- 1 | require "string_pool" 2 | require "./token" 3 | require "./lexer_context" 4 | 5 | class GraphQL::Language::Lexer 6 | def lex(source : String | IO) 7 | lex(source, 0) 8 | end 9 | 10 | def lex(source : String | IO, start_position : Int32) : Token 11 | context = Language::LexerContext.new(source, start_position) 12 | context.get_token 13 | end 14 | 15 | def self.lex(source : String | IO) 16 | GraphQL::Language::Lexer.new.lex(source) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/graphql-crystal/language/lexer_context.cr: -------------------------------------------------------------------------------- 1 | class GraphQL::Language::LexerContext 2 | def initialize(source : String, index : Int32) 3 | @current_index = index 4 | @source = source 5 | end 6 | 7 | def get_token : Token 8 | return create_eof_token() if @source.nil? 9 | 10 | @current_index = get_position_after_whitespace(@source, @current_index) 11 | 12 | return create_eof_token() if @current_index >= @source.size 13 | 14 | code = @source[@current_index] 15 | 16 | validate_character_code(code) 17 | 18 | token = check_for_punctuation_tokens(code) 19 | 20 | return token unless token.nil? 21 | 22 | return read_comment() if code == '#' 23 | return read_name() if code.letter? || code == '_' 24 | return read_number() if code.number? || code == '-' 25 | return read_string() if code == '"' 26 | 27 | raise Exception.new("Unexpected character '#{code}' at #{@current_index} near #{@source[@current_index-15,30]}") 28 | end 29 | 30 | def only_hex_in_string(test) 31 | !!test.match(/\A\b[0-9a-fA-F]+\b\Z/) 32 | end 33 | 34 | def read_comment : Token 35 | start = @current_index 36 | chunk_start = (@current_index += 1) 37 | 38 | code = get_code 39 | value = "" 40 | 41 | while is_not_at_the_end_of_query() && code.ord != 0x000A && code.ord != 0x000D 42 | code = process_character(pointerof(value), pointerof(chunk_start)) 43 | end 44 | 45 | value += @source[chunk_start, (@current_index - chunk_start)] 46 | 47 | Token.new(Token::Kind::COMMENT, value.strip, start, @current_index + 1) 48 | end 49 | 50 | def read_number : Token 51 | is_float = false 52 | start = @current_index 53 | code = @source[start] 54 | code = self.next_code if code == '-' 55 | next_code_char = code == '0' ? self.next_code : read_digits_from_own_source(code) 56 | raise Exception.new("Invalid number, unexpected digit after #{code}: #{next_code_char}") if (next_code_char.ord >= 48 && next_code_char.ord <= 57) 57 | 58 | code = next_code_char 59 | if code == '.' 60 | is_float = true 61 | code = read_digits_from_own_source(self.next_code()) 62 | end 63 | 64 | if code == 'E' || code == 'e' 65 | is_float = true 66 | code = self.next_code() 67 | if code == '+' || code == '-' 68 | code = self.next_code() 69 | end 70 | code = read_digits_from_own_source(code) 71 | end 72 | 73 | is_float ? create_float_token(start) 74 | : create_int_token(start) 75 | end 76 | 77 | def read_string : Token 78 | start = @current_index 79 | value = process_string_chunks() 80 | 81 | Token.new(Token::Kind::STRING, value, start, @current_index + 1) 82 | end 83 | 84 | private def is_valid_name_character(code) : Bool 85 | code == '_' || code.alphanumeric? 86 | end 87 | 88 | private def append_characters_from_last_chunk(value, chunk_start) 89 | value + @source[chunk_start, (@current_index - chunk_start - 1)] 90 | end 91 | 92 | private def append_to_value_by_code(value, code) 93 | case code 94 | when '"' 95 | value += '"' 96 | when '/' 97 | value += '/' 98 | when '\\' 99 | value += '\\' 100 | when 'b' 101 | value += '\b' 102 | when 'f' 103 | value += '\f' 104 | when 'n' 105 | value += '\n' 106 | when 'r' 107 | value += '\r' 108 | when 't' 109 | value += '\t' 110 | when 'u' 111 | value += get_unicode_char 112 | else 113 | raise Exception.new("Invalid character escape sequence: \\#{code}.") 114 | end 115 | end 116 | 117 | private def check_for_invalid_characters(code) 118 | raise Exception.new("Invalid character within String: #{code}.") if code.ord < 0x0020 && code.ord != 0x0009 119 | end 120 | 121 | private def check_for_punctuation_tokens(code) 122 | case code 123 | when '!' 124 | create_punctuation_token(Token::Kind::BANG, 1) 125 | when '$' 126 | create_punctuation_token(Token::Kind::DOLLAR, 1) 127 | when '(' 128 | create_punctuation_token(Token::Kind::PAREN_L, 1) 129 | when ')' 130 | create_punctuation_token(Token::Kind::PAREN_R, 1) 131 | when '.' 132 | check_for_spread_operator() 133 | when ':' 134 | create_punctuation_token(Token::Kind::COLON, 1) 135 | when '=' 136 | create_punctuation_token(Token::Kind::EQUALS, 1) 137 | when '@' 138 | create_punctuation_token(Token::Kind::AT, 1) 139 | when '[' 140 | create_punctuation_token(Token::Kind::BRACKET_L, 1) 141 | when ']' 142 | create_punctuation_token(Token::Kind::BRACKET_R, 1) 143 | when '{' 144 | create_punctuation_token(Token::Kind::BRACE_L, 1) 145 | when '|' 146 | create_punctuation_token(Token::Kind::PIPE, 1) 147 | when '}' 148 | create_punctuation_token(Token::Kind::BRACE_R, 1) 149 | end 150 | end 151 | 152 | private def check_for_spread_operator : Token? 153 | char1 = @source.size > @current_index + 1 ? @source[@current_index + 1] : 0 154 | char2 = @source.size > @current_index + 2 ? @source[@current_index + 2] : 0 155 | 156 | return create_punctuation_token(Token::Kind::SPREAD, 3) if char1 == '.' && char2 == '.' 157 | end 158 | 159 | private def check_string_termination(code) 160 | raise Exception.new("Unterminated string.") if code != '"' 161 | end 162 | 163 | private def create_eof_token : Token 164 | Token.new(Token::Kind::EOF, nil, @current_index, @current_index) 165 | end 166 | 167 | private def create_float_token(start) : Token 168 | Token.new(Token::Kind::FLOAT, @source[start, (@current_index - start)], start, @current_index) 169 | end 170 | 171 | private def create_int_token(start) : Token 172 | Token.new(Token::Kind::INT, @source[start, (@current_index - start)], start, @current_index) 173 | end 174 | 175 | private def create_name_token(start) : Token 176 | Token.new(Token::Kind::NAME, @source[start, (@current_index - start)], start, @current_index) 177 | end 178 | 179 | private def create_punctuation_token(kind, offset) : Token 180 | Token.new(kind, nil, @current_index, @current_index + offset) 181 | end 182 | 183 | private def get_position_after_whitespace(body : String, start) 184 | position = start 185 | 186 | while position < body.size 187 | code = body[position] 188 | case code 189 | when '\uFEFF', '\t', ' ', '\n', '\r', ',' 190 | position += 1 191 | else 192 | return position 193 | end 194 | end 195 | 196 | position 197 | end 198 | 199 | private def get_unicode_char 200 | if @current_index + 5 > @source.size 201 | truncated_expression = @source[@current_index, @source.size] 202 | raise Exception.new("Invalid character escape sequence at EOF: \\#{truncated_expression}.") 203 | end 204 | 205 | expression = @source[@current_index, 5] 206 | 207 | if !only_hex_in_string(expression[1, expression.size]) 208 | raise Exception.new("Invalid character escape sequence: \\#{expression}.") 209 | end 210 | 211 | s = next_code.bytes << 12 | next_code.bytes << 8 | next_code.bytes << 4 | next_code.bytes 212 | String.new(Slice.new(s.to_unsafe, 4))[0] 213 | end 214 | 215 | private def if_unicode_get_string : String 216 | return @source.size > @current_index + 5 && 217 | only_hex_in_string(@source[(@current_index + 2), 4]) ? @source[@current_index, 6] : null 218 | end 219 | 220 | private def is_not_at_the_end_of_query 221 | @current_index < @source.size 222 | end 223 | 224 | private def next_code 225 | @current_index += 1 226 | return is_not_at_the_end_of_query() ? @source[@current_index] : Char::ZERO 227 | end 228 | 229 | private def process_character(value_ptr, chunk_start_ptr) 230 | code = get_code 231 | @current_index += 1 232 | 233 | if code == '\\' 234 | value_ptr.value = append_to_value_by_code(append_characters_from_last_chunk(value_ptr.value, chunk_start_ptr.value), get_code) 235 | 236 | @current_index += 1 237 | chunk_start_ptr.value = @current_index 238 | end 239 | 240 | return get_code 241 | end 242 | 243 | private def process_string_chunks 244 | chunk_start = (@current_index += 1) 245 | code = get_code 246 | value = "" 247 | 248 | while is_not_at_the_end_of_query() && code.ord != 0x000A && code.ord != 0x000D && code != '"' 249 | check_for_invalid_characters(code) 250 | code = process_character(pointerof(value), pointerof(chunk_start)) 251 | end 252 | 253 | check_string_termination(code) 254 | value += @source[chunk_start, (@current_index - chunk_start)] 255 | value 256 | end 257 | 258 | private def read_digits(source, start, first_code) 259 | body = source 260 | position = start 261 | code = first_code 262 | 263 | if !code.number? 264 | raise Exception.new("Invalid number, expected digit but got: #{resolve_char_name(code)}") 265 | end 266 | 267 | while true 268 | code = (position += 1) < body.size ? body[position] : Char::ZERO 269 | break unless code.number? 270 | end 271 | 272 | position 273 | end 274 | 275 | private def read_digits_from_own_source(code) 276 | @current_index = read_digits(@source, @current_index, code) 277 | get_code 278 | end 279 | 280 | private def read_name 281 | start = @current_index 282 | code = Char::ZERO 283 | 284 | while true 285 | @current_index += 1 286 | code = get_code 287 | break unless is_not_at_the_end_of_query && is_valid_name_character(code) 288 | end 289 | 290 | create_name_token(start) 291 | end 292 | 293 | private def resolve_char_name(code, unicode_string = nil) 294 | return "" if (code == '\0') 295 | 296 | return "\"#{unicode_string}\"" if unicode_string && !unicode_string.blank? 297 | return "\"#{code}\"" 298 | end 299 | 300 | private def validate_character_code(code) 301 | i32_code = code.ord 302 | if (i32_code < 0x0020) && (i32_code != 0x0009) && (i32_code != 0x000A) && (i32_code != 0x000D) 303 | raise Exception.new("Invalid character \"\\u#{code}\".") 304 | end 305 | end 306 | 307 | private def wait_for_end_of_comment(body, position, code) 308 | while (position += 1) < body.size && (code = body[position]) != 0 && (code.ord > 0x001F || code.ord == 0x0009) && code.ord != 0x000A && code.ord != 0x000D 309 | end 310 | 311 | return position 312 | end 313 | 314 | private def get_code 315 | return is_not_at_the_end_of_query ? @source[@current_index] : Char::ZERO 316 | end 317 | end -------------------------------------------------------------------------------- /src/graphql-crystal/language/nodes.cr: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require "./ast" 3 | require "./generation" 4 | require "../types/object_type" 5 | 6 | module GraphQL 7 | module Language 8 | macro define_array_cast(type) 9 | def self.to_{{type.id.downcase}}(value : Array) : {{type.id}} 10 | _values = [] of {{type.id}} 11 | value.each do |val| 12 | _values << to_{{type.id.downcase}}(val) 13 | end 14 | _values 15 | end 16 | 17 | def self.to_{{type.id.downcase}}(value) {{type.id}} 18 | value.as({{type.id}}) 19 | end 20 | 21 | def self.to_fvalue(v : NullValue): Nil 22 | nil 23 | end 24 | 25 | def self.to_argumentvalue(v : NullValue): Nil 26 | nil 27 | end 28 | end 29 | 30 | class AbstractNode < ASTNode 31 | # this works only if the module 32 | # gets included in the class exactly 33 | # at this file 34 | include GraphQL::ObjectType 35 | 36 | # getter :line, :col 37 | # @col : Int32 38 | # @line : Int32 39 | # 40 | # def initialize(**options) 41 | # position_source = options.fetch(:position_source, nil) 42 | # if position_source 43 | # # @line, @col = position_source.as(Array(Int32)).line_and_column 44 | # end 45 | # super() 46 | # end 47 | end 48 | 49 | # This is the AST root for normal queries 50 | # 51 | # @example Deriving a document by parsing a string 52 | # document = GraphQL.parse(query_string) 53 | # 54 | # @example Creating a string from a document 55 | # document.to_query_string 56 | # # { ... } 57 | # 58 | class Document < AbstractNode 59 | values({definitions: Array( 60 | OperationDefinition | FragmentDefinition | SchemaDefinition | ObjectTypeDefinition | InputObjectTypeDefinition | 61 | ScalarTypeDefinition | DirectiveDefinition | EnumTypeDefinition | InterfaceTypeDefinition | UnionTypeDefinition 62 | )}) 63 | traverse :children, :definitions 64 | 65 | def to_query_string 66 | GraphQL::Language::Generation.generate(self) 67 | end 68 | # def slice_definition(name) 69 | # GraphQL::Language::DefinitionSlice.slice(self, name) 70 | # end 71 | end 72 | 73 | class SchemaDefinition < AbstractNode 74 | values({query: String, mutation: String?, subscription: String?}) 75 | end 76 | 77 | # A query, mutation or subscription. 78 | # May be anonymous or named. 79 | # May be explicitly typed (eg `mutation { ... }`) or implicitly a query (eg `{ ... }`). 80 | class OperationDefinition < AbstractNode 81 | values( 82 | { 83 | operation_type: String, 84 | name: String?, 85 | variables: Array(VariableDefinition), 86 | directives: Array(Directive), 87 | selections: Array(Selection), 88 | } 89 | ) 90 | traverse :children, :variables, :directives, :selections 91 | end 92 | 93 | class DirectiveDefinition < AbstractNode 94 | values({name: String, arguments: Array(InputValueDefinition), locations: Array(String), description: String?}) 95 | traverse :children, :arguments 96 | end 97 | 98 | class Directive < AbstractNode 99 | values({name: String, arguments: Array(Argument)}) 100 | traverse :children, :arguments 101 | end 102 | 103 | alias FValue = String | Int32 | Float64 | Bool | Nil | AEnum | InputObject | Array(FValue) | Hash(String, FValue) 104 | 105 | define_array_cast(FValue) 106 | 107 | alias Type = TypeName | NonNullType | ListType 108 | alias Selection = Field | FragmentSpread | InlineFragment 109 | 110 | class VariableDefinition < AbstractNode 111 | values({name: String, type: Type, default_value: FValue}) 112 | traverse :children, :type 113 | end 114 | 115 | alias ArgumentValue = FValue | InputObject | VariableIdentifier | Array(ArgumentValue) | ReturnType 116 | 117 | define_array_cast(ArgumentValue) 118 | 119 | class Argument < AbstractNode 120 | values({name: String, value: ArgumentValue}) 121 | 122 | def to_value 123 | value 124 | end 125 | end 126 | 127 | class TypeDefinition < AbstractNode 128 | values({name: String, description: String?}) 129 | end 130 | 131 | class ScalarTypeDefinition < TypeDefinition 132 | values({directives: Array(Directive)}) 133 | traverse :children, :directives 134 | end 135 | 136 | class ObjectTypeDefinition < TypeDefinition 137 | values( 138 | {interfaces: Array(String), 139 | fields: Array(FieldDefinition), 140 | directives: Array(Directive)} 141 | ) 142 | traverse :children, :fields, :directives 143 | end 144 | 145 | class InputObjectTypeDefinition < TypeDefinition 146 | values({fields: Array(InputValueDefinition), directives: Array(Directive)}) 147 | traverse :children, :fields, :directives 148 | end 149 | 150 | class InputValueDefinition < AbstractNode 151 | values({name: String, type: Type, default_value: FValue, directives: Array(Directive), description: String?}) 152 | traverse :children, :type, :directives 153 | end 154 | 155 | # Base class for nodes whose only value is a name (no child nodes or other scalars) 156 | class NameOnlyNode < AbstractNode 157 | values({name: String}) 158 | end 159 | 160 | # Base class for non-null type names and list type names 161 | class WrapperType < AbstractNode 162 | values({of_type: (Type)}) 163 | traverse :children, :of_type 164 | end 165 | 166 | # A type name, used for variable definitions 167 | class TypeName < NameOnlyNode; end 168 | 169 | # A list type definition, denoted with `[...]` (used for variable type definitions) 170 | class ListType < WrapperType; end 171 | 172 | # A collection of key-value inputs which may be a field argument 173 | 174 | class InputObject < AbstractNode 175 | values({arguments: Array(Argument)}) 176 | traverse :children, :arguments 177 | 178 | # @return [Hash] Recursively turn this input object into a Ruby Hash 179 | def to_h 180 | arguments.reduce({} of String => FValue) do |memo, pair| 181 | v = pair.value 182 | memo[pair.name] = case v 183 | when InputObject 184 | v.to_h 185 | when Array 186 | v.map { |v| v.as(FValue) } 187 | else 188 | v 189 | end.as(FValue) 190 | memo 191 | end 192 | end 193 | 194 | def to_value 195 | to_h 196 | end 197 | end 198 | 199 | # A non-null type definition, denoted with `...!` (used for variable type definitions) 200 | class NonNullType < WrapperType; end 201 | 202 | # An enum value. The string is available as {#name}. 203 | class AEnum < NameOnlyNode 204 | def to_value 205 | name 206 | end 207 | end 208 | 209 | # A null value literal. 210 | class NullValue < NameOnlyNode; end 211 | 212 | class VariableIdentifier < NameOnlyNode; end 213 | 214 | # A single selection in a 215 | # A single selection in a GraphQL query. 216 | class Field < AbstractNode 217 | values({ 218 | name: String, 219 | _alias: String?, 220 | arguments: Array(Argument), 221 | directives: Array(Directive), 222 | selections: Array(Selection), 223 | }) 224 | traverse :children, :arguments, :directives, :selections 225 | end 226 | 227 | class FragmentDefinition < AbstractNode 228 | values({ 229 | name: String?, 230 | type: Type, 231 | directives: Array(Directive), 232 | selections: Array(Selection), 233 | }) 234 | traverse :children, :type, :directives, :selections 235 | end 236 | 237 | class FieldDefinition < AbstractNode 238 | values({name: String, arguments: Array(InputValueDefinition), type: Type, directives: Array(Directive), description: String?}) 239 | traverse :children, :type, :arguments, :directives 240 | end 241 | 242 | class InterfaceTypeDefinition < TypeDefinition 243 | values({fields: Array(FieldDefinition), directives: Array(Directive)}) 244 | traverse :children, :fields, :directives 245 | end 246 | 247 | class UnionTypeDefinition < TypeDefinition 248 | values({types: Array(TypeName), directives: Array(Directive)}) 249 | traverse :children, :types, :directives 250 | end 251 | 252 | class EnumTypeDefinition < TypeDefinition 253 | values({fvalues: Array(EnumValueDefinition), directives: Array(Directive)}) 254 | traverse :children, :directives 255 | end 256 | 257 | # Application of a named fragment in a selection 258 | class FragmentSpread < AbstractNode 259 | values({name: String, directives: Array(Directive)}) 260 | traverse :children, :directives 261 | end 262 | 263 | # An unnamed fragment, defined directly in the query with `... { }` 264 | class InlineFragment < AbstractNode 265 | values({type: Type?, directives: Array(Directive), selections: Array(Selection)}) 266 | traverse :children, :type, :directives, :selections 267 | end 268 | 269 | class EnumValueDefinition < AbstractNode 270 | values({name: String, directives: Array(Directive), selection: Array(Selection)?, description: String?}) 271 | traverse :children, :directives 272 | end 273 | end 274 | end 275 | -------------------------------------------------------------------------------- /src/graphql-crystal/language/parser.cr: -------------------------------------------------------------------------------- 1 | require "./parser_context" 2 | 3 | class GraphQL::Language::Parser 4 | property max_nesting = 512 5 | 6 | def initialize(lexer : Language::Lexer) 7 | @lexer = lexer 8 | end 9 | 10 | def parse(source : String) : Language::Document 11 | context = Language::ParserContext.new(source, @lexer) 12 | context.parse 13 | end 14 | end -------------------------------------------------------------------------------- /src/graphql-crystal/language/token.cr: -------------------------------------------------------------------------------- 1 | class GraphQL::Language::Token 2 | enum Kind 3 | BANG 4 | DOLLAR 5 | PAREN_L 6 | PAREN_R 7 | SPREAD 8 | COLON 9 | EQUALS 10 | AT 11 | BRACKET_L 12 | BRACKET_R 13 | BRACE_L 14 | PIPE 15 | BRACE_R 16 | NAME 17 | INT 18 | FLOAT 19 | STRING 20 | BLOCK_STRING 21 | COMMENT 22 | AMP 23 | EOF 24 | end 25 | 26 | property kind : Kind 27 | property start_position : Int32 28 | property end_position : Int32 29 | property value : String? 30 | 31 | def initialize(kind, value, start_position, end_position) 32 | @kind = kind 33 | @start_position = start_position 34 | @end_position = end_position 35 | @value = value 36 | end 37 | 38 | def self.get_token_kind_description(kind : Kind) : String 39 | case kind 40 | when Kind::EOF 41 | "EOF" 42 | when Kind::BANG 43 | "!" 44 | when Kind::DOLLAR 45 | "$" 46 | when Kind::PAREN_L 47 | "(" 48 | when Kind::PAREN_R 49 | ")" 50 | when Kind::SPREAD 51 | "..." 52 | when Kind::COLON 53 | ":" 54 | when Kind::EQUALS 55 | "=" 56 | when Kind::AT 57 | "@" 58 | when Kind::BRACKET_L 59 | "[" 60 | when Kind::BRACKET_R 61 | "]" 62 | when Kind::BRACE_L 63 | "{" 64 | when Kind::PIPE 65 | "|" 66 | when Kind::BRACE_R 67 | "}" 68 | when Kind::NAME 69 | "Name" 70 | when Kind::INT 71 | "Int" 72 | when Kind::FLOAT 73 | "Float" 74 | when Kind::STRING 75 | "String" 76 | when Kind::COMMENT 77 | "#" 78 | else 79 | ""; 80 | end 81 | end 82 | end -------------------------------------------------------------------------------- /src/graphql-crystal/language/type.cr: -------------------------------------------------------------------------------- 1 | require "./nodes" 2 | 3 | module CLTK 4 | alias TokenValue = String | Float64 | Int32 | Nil 5 | alias Type = GraphQL::Language::AbstractNode | 6 | TokenValue | 7 | Bool | 8 | Tuple(String, String) | 9 | Array(Type) 10 | end 11 | -------------------------------------------------------------------------------- /src/graphql-crystal/schema.cr: -------------------------------------------------------------------------------- 1 | require "./language" 2 | require "./types/type_validation" 3 | require "./types/object_type" 4 | require "./schema/schema" 5 | require "./schema/*" 6 | 7 | # require "./schema/schema_execute" 8 | # require "./schema/validation" 9 | # require "./schema/variable_resolver.cr" 10 | # require "./schema/fragment_resolver" 11 | module GraphQL 12 | # Record the message and path of a resolution error 13 | alias Error = {message: String, path: Array(String | Int32)} 14 | 15 | module Schema 16 | # Instantiate the `Schema` class from a 17 | # String that represents a graphql-schema in 18 | # the graphql schema definition language 19 | def self.from_schema(schema_string) 20 | Schema.new GraphQL::Language.parse(schema_string) 21 | end 22 | 23 | # a struct that can be inherited from 24 | # when defining custom InputType structs 25 | # for conveniently accessing query parameters 26 | abstract struct InputType 27 | macro inherited 28 | def_clone 29 | end 30 | # abstract def self.from_json(json) : InputType 31 | end 32 | 33 | struct AlibiType < InputType 34 | JSON.mapping({some: Bool}) 35 | end 36 | end 37 | 38 | private alias ReturnType = String | Int32 | Int64 | Float64 | Bool | Nil | Array(ReturnType) | Hash(String, ReturnType) | 39 | Schema::InputType 40 | private alias ResolveCBReturnType = ReturnType | ObjectType | Nil | Array(ResolveCBReturnType) 41 | end 42 | -------------------------------------------------------------------------------- /src/graphql-crystal/schema/fragment_resolver.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Schema 3 | module FragmentResolver 4 | # replace named fragments with their concrete selections before 5 | # the query is resolved 6 | def self.resolve(value, fragments) 7 | visit(value, fragments) 8 | end 9 | 10 | private def self.visit(values : Array, fragments : Array(Language::FragmentDefinition)) 11 | new_values = Array(Language::AbstractNode).new 12 | values.each { |v| new_values = new_values + [visit(v, fragments)].flatten } 13 | new_values 14 | end 15 | 16 | private def self.visit(value : Language::Field, fragments) 17 | new_values = Array(Language::AbstractNode).new 18 | value.selections.each do |s| 19 | new_values = new_values + visit(s, fragments).map &.as(Language::AbstractNode) 20 | end 21 | value.selections = new_values.flatten.map { |v| v.as(Language::Selection) } 22 | [value] 23 | end 24 | 25 | private def self.visit(value : Language::FragmentSpread, fragments) 26 | fragment_definition = fragments.find(&.name.==(value.name)) 27 | raise "fragment \"#{value.name}\" is undefined" unless fragment_definition 28 | fragment_definition.selections.map do |sel| 29 | if sel.responds_to? :directives 30 | sel.directives = value.directives 31 | end 32 | sel 33 | end 34 | end 35 | 36 | # Inline fragments will be resolved inline as they carry all the 37 | # information needed to validate and apply them with a concrete 38 | # object further down the line of then object type resolution 39 | private def self.visit(value : Language::InlineFragment, fragments) 40 | [value] 41 | end 42 | 43 | private def self.visit(value, fragments) 44 | raise "I should have never arrived here!" 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/graphql-crystal/schema/introspection_query.cr: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # The introspection query to end all introspection queries, copied from 3 | # https://github.com/graphql/graphql-js/blob/master/src/utilities/introspectionQuery.js 4 | module GraphQL 5 | module Schema 6 | INTROSPECTION_QUERY = <<-intro_query 7 | query IntrospectionQuery { 8 | __schema { 9 | queryType { name } 10 | mutationType { name } 11 | subscriptionType { name } 12 | types { 13 | ...FullType 14 | } 15 | directives { 16 | name 17 | description 18 | locations 19 | args { 20 | ...InputValue 21 | } 22 | } 23 | } 24 | } 25 | fragment FullType on __Type { 26 | kind 27 | name 28 | description 29 | fields(includeDeprecated: true) { 30 | name 31 | description 32 | args { 33 | ...InputValue 34 | } 35 | type { 36 | ...TypeRef 37 | } 38 | isDeprecated 39 | deprecationReason 40 | } 41 | inputFields { 42 | ...InputValue 43 | } 44 | interfaces { 45 | ...TypeRef 46 | } 47 | enumValues(includeDeprecated: true) { 48 | name 49 | description 50 | isDeprecated 51 | deprecationReason 52 | } 53 | possibleTypes { 54 | ...TypeRef 55 | } 56 | } 57 | fragment InputValue on __InputValue { 58 | name 59 | description 60 | type { ...TypeRef } 61 | defaultValue 62 | } 63 | fragment TypeRef on __Type { 64 | kind 65 | name 66 | ofType { 67 | kind 68 | name 69 | ofType { 70 | kind 71 | name 72 | ofType { 73 | kind 74 | name 75 | ofType { 76 | kind 77 | name 78 | ofType { 79 | kind 80 | name 81 | ofType { 82 | kind 83 | name 84 | ofType { 85 | kind 86 | name 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | intro_query 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /src/graphql-crystal/schema/middleware.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Schema 3 | module Middleware 4 | property next : Middleware | Proc | Nil 5 | 6 | abstract def call( 7 | node : GraphQL::Language::TypeDefinition | GraphQL::Language::FieldDefinition, 8 | selection : Array(Language::Selection), 9 | object : ResolveCBReturnType, context : Context 10 | ) 11 | 12 | private def call_next(*args) 13 | next_handler = @next 14 | if next_handler 15 | next_handler.call(args[0], args[1], args[2].as(ResolveCBReturnType), args[3]) 16 | else 17 | raise "incomplete middleware chain" 18 | end 19 | end 20 | 21 | alias Proc = Language::AbstractNode, Array(Language::AbstractNode), ResolveCBReturnType, Context -> {ReturnType, Array(GraphQL::Error)} 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/graphql-crystal/schema/schema.cr: -------------------------------------------------------------------------------- 1 | require "./schema_introspection" 2 | 3 | module GraphQL 4 | module Schema 5 | # 6 | # The Context that will be created when `Schema::execute` is called 7 | # and provided as an argument to the field resolution callbacks on 8 | # Object Types. Can be subclassed and passed manually to `Schema::execute`. 9 | # 10 | class Context 11 | getter :schema 12 | @max_depth : Int32? 13 | @depth = 0 14 | @fragments : Array(Language::FragmentDefinition) = [] of Language::FragmentDefinition 15 | property :max_depth, :depth, :fragments 16 | 17 | def initialize(@schema : GraphQL::Schema::Schema, @max_depth = nil); end 18 | 19 | def with_self(args) 20 | yield(args, self) 21 | end 22 | end 23 | 24 | private alias QueryReturnType = Array(GraphQL::ObjectType?) | GraphQL::ObjectType | ReturnType | Nil 25 | 26 | # 27 | # Represents a GraphQL Schema against which queries can be executed. 28 | # 29 | class Schema 30 | include GraphQL::Schema::Introspection 31 | getter :types, :directive_middlewares, 32 | :directive_definitions, :max_depth 33 | property :query_resolver, :mutation_resolver 34 | 35 | # max recursive execution depth 36 | @max_depth : Int32? = nil 37 | 38 | # holds root query object 39 | @query_resolver : GraphQL::ObjectType? 40 | # holds root mutation object 41 | @mutation_resolver : GraphQL::ObjectType? 42 | 43 | # a index of all types defined in the schema 44 | @types : Hash(String, Language::TypeDefinition) 45 | 46 | # holds structs for input type parsing 47 | @input_types = Hash(String, InputType.class).new 48 | 49 | # holds definitions of all directives used in the schema 50 | @directive_definitions = Hash(String, Language::DirectiveDefinition).new 51 | 52 | # directive middlewares to be evaluated 53 | # during query execution 54 | @directive_middlewares = [ 55 | GraphQL::Directives::IncludeDirective.new, 56 | GraphQL::Directives::SkipDirective.new, 57 | ] 58 | 59 | # an instance of `GraphQL::TypeValidation` 60 | # used for validating inputs against the 61 | # schema definition 62 | @type_validation : GraphQL::TypeValidation 63 | 64 | # 65 | # Takes a parsed GraphQL schema definition 66 | # 67 | def initialize(@document : Language::Document) 68 | schema, @types, @directive_definitions = extract_elements 69 | 70 | # substitute TypeNames with type definition 71 | @query_definition = @types[schema.query]?.as(Language::ObjectTypeDefinition?) 72 | 73 | @type_validation = GraphQL::TypeValidation.new(@types) 74 | end 75 | 76 | # 77 | # Descriptions for Scalar Types 78 | # 79 | ScalarTypes = { 80 | {"String", "A String Value"}, 81 | {"Boolean", "A Boolean Value"}, 82 | {"Int", "An Integer Number"}, 83 | {"Float", "A Floating Point Number"}, 84 | {"ID", "An ID"}, 85 | } 86 | 87 | # 88 | # register a Struct to parse query variables 89 | # name : the name of the GraphQL Input Type that gets parsed 90 | # type : the Struct Type to parse the JSON into 91 | # (has to have the class method from_json see 92 | # https://crystal-lang.org/api/0.23.1/JSON.html#mapping%28properties%2Cstrict%3Dfalse%29-macro) 93 | # for more infos 94 | def add_input_type(name : String, type : InputType.class) 95 | @input_types[name] = type 96 | end 97 | 98 | # get a type definition 99 | # 100 | def type_resolve(type : String | Language::AbstractNode) 101 | case type 102 | when String 103 | @types[type] 104 | when Language::TypeName 105 | @types[type.name] 106 | else 107 | type 108 | end 109 | end 110 | 111 | private def extract_elements(node = @document) 112 | types = Hash(String, Language::TypeDefinition).new 113 | directives = Hash(String, Language::DirectiveDefinition).new 114 | 115 | schema = uninitialized Language::SchemaDefinition 116 | 117 | ScalarTypes.each do |(type_name, description)| 118 | types[type_name] = Language::ScalarTypeDefinition.new( 119 | name: type_name, description: description, 120 | directives: [] of Language::Directive 121 | ) 122 | end 123 | 124 | node.map_children do |node| 125 | case node 126 | when Language::SchemaDefinition 127 | schema = node 128 | when Language::TypeDefinition 129 | types[node.name] = node 130 | when Language::DirectiveDefinition 131 | directives[node.name] = node 132 | end 133 | node 134 | end 135 | return {schema, types, directives} 136 | end 137 | 138 | def max_depth(@max_depth); end 139 | 140 | def resolve 141 | with self yield 142 | self 143 | end 144 | 145 | private def cast_wrap_block(&block : Hash(String, ReturnType) -> _) 146 | Proc(Hash(String, ReturnType), GraphQL::Schema::Context, QueryReturnType).new do |args, context| 147 | res = context.with_self args, &block 148 | ( 149 | res.is_a?(Array) ? res.map(&.as(GraphQL::ObjectType?)) : res 150 | ).as(QueryReturnType) 151 | end 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /src/graphql-crystal/schema/schema_introspection.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Schema 3 | module Introspection 4 | # 5 | # Wrap an ObjectType intercepting field 6 | # resolution for `__schema` and `__type` 7 | # keys 8 | # 9 | class IntrospectionObject 10 | include ObjectType 11 | @query_resolver : GraphQL::ObjectType 12 | property :query_resolver, :mutation_resolver 13 | 14 | def initialize(@schema : GraphQL::Schema::Schema, @query_resolver); end 15 | 16 | def schema=(@schema); end 17 | 18 | def graphql_type 19 | @query_resolver.graphql_type 20 | end 21 | 22 | def resolve_field(name, args, context) 23 | case name 24 | when "__schema" 25 | @schema 26 | when "__type" 27 | @schema.types[args["name"]] 28 | else 29 | @query_resolver.resolve_field(name, args, context) 30 | end 31 | end 32 | end 33 | 34 | macro included 35 | include ObjectType 36 | 37 | field :types { @original_types.not_nil! } 38 | field :directives { @directive_definitions.values } 39 | # subscriptionType is not supported atm. 40 | field :subscriptionType { nil } 41 | field :queryType { @original_query_definition } 42 | field :mutationType { @mutation_resolver ? @types[@mutation_resolver.as(ObjectType).graphql_type] : nil } 43 | 44 | macro finished 45 | # a clone of @query with the 46 | # meta fields removed 47 | @original_query_definition : Language::ObjectTypeDefinition? 48 | @original_types : Array(Language::TypeDefinition)? 49 | 50 | def initialize(document : Language::Document) 51 | previous_def(document) 52 | @original_query_definition = 53 | types[@query_definition.not_nil!.name] 54 | .clone.as(Language::ObjectTypeDefinition) 55 | 56 | # add introspection types to 57 | # schemas types index 58 | _schema, _types, _directives = extract_elements( 59 | GraphQL::Language.parse(INTROSPECTION_TYPES) 60 | ) 61 | @types.merge!( _types.reject("schema") ) 62 | @directive_definitions.merge! _directives 63 | 64 | # keep the original query within the 65 | # array used for introspection 66 | @original_types = types.values.compact_map do |t| 67 | ( t.name == @query_definition.try &.name ) ? 68 | @original_query_definition : t 69 | end 70 | 71 | # add the schema field to the root query of 72 | # the schema 73 | if root_query = @query_definition 74 | root_query.fields << Language::FieldDefinition.new( 75 | "__schema", Array(Language::InputValueDefinition).new, 76 | Language::TypeName.new(name: "__Schema"), Array(Language::Directive).new, 77 | "query the schema served at this endpoint" 78 | ) 79 | root_query.fields << Language::FieldDefinition.new( 80 | "__type", [ 81 | Language::InputValueDefinition.new( 82 | name: "name", type: Language::TypeName.new(name: "String"), default_value: nil, 83 | directives: [] of Language::Directive, description: "") 84 | ], Language::TypeName.new(name: "__Type"), Array(Language::Directive).new, 85 | "query a specific type in the schema by name" 86 | ) 87 | end 88 | end 89 | 90 | # 91 | # Wrap the Root Query in the IntrospectionObject 92 | # to intercept calls to __schema and __type field 93 | def query_resolver=(query : ObjectType) 94 | @query_resolver = IntrospectionObject.new(self, query) 95 | end 96 | 97 | end 98 | 99 | end 100 | 101 | INTROSPECTION_TYPES = <<-schema 102 | 103 | # A String Value 104 | scalar String 105 | 106 | # A Boolean Value 107 | scalar Boolean 108 | 109 | # An Integer Number 110 | scalar Int 111 | 112 | # A Floating Point Number 113 | scalar Float 114 | 115 | # An ID 116 | scalar ID 117 | 118 | # Optionally includes selection from the result set 119 | directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 120 | # Optionally excludes selection from the result set 121 | directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 122 | # Marks an element of a GraphQL schema as no longer supported. 123 | directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE 124 | 125 | type __Schema { 126 | types: [__Type!]! 127 | queryType: __Type! 128 | mutationType: __Type 129 | subscriptionType: __Type 130 | directives: [__Directive!]! 131 | } 132 | 133 | type __Type { 134 | kind: __TypeKind! 135 | name: String 136 | description: String 137 | # OBJECT and INTERFACE only 138 | fields(includeDeprecated: Boolean = false): [__Field!] 139 | # OBJECT only 140 | interfaces: [__Type!] 141 | # INTERFACE and UNION only 142 | possibleTypes: [__Type!] 143 | # ENUM only 144 | enumValues(includeDeprecated: Boolean = false): [__EnumValue!] 145 | # INPUT_OBJECT only 146 | inputFields: [__InputValue!] 147 | # NON_NULL and LIST only 148 | ofType: __Type 149 | } 150 | 151 | type __Field { 152 | name: String! 153 | description: String 154 | args: [__InputValue!]! 155 | type: __Type! 156 | isDeprecated: Boolean! 157 | deprecationReason: String 158 | } 159 | 160 | type __InputValue { 161 | name: String! 162 | description: String 163 | type: __Type! 164 | defaultValue: String 165 | } 166 | 167 | type __EnumValue { 168 | name: String! 169 | description: String 170 | isDeprecated: Boolean! 171 | deprecationReason: String 172 | } 173 | 174 | type __Directive { 175 | name: String! 176 | description: String 177 | args: [__InputValue!]! 178 | locations: [__DirectiveLocation!]! 179 | onOperation: Boolean! 180 | onFragment: Boolean! 181 | onField: Boolean! 182 | } 183 | 184 | enum __TypeKind { 185 | SCALAR 186 | OBJECT 187 | INTERFACE 188 | UNION 189 | ENUM 190 | INPUT_OBJECT 191 | LIST 192 | NON_NULL 193 | } 194 | 195 | # A Directive can be adjacent to many parts 196 | # of the GraphQL language, a __DirectiveLocation 197 | # describes one such possible adjacencies. 198 | enum __DirectiveLocation { 199 | # Location adjacent 200 | # to a query operation 201 | QUERY 202 | # Location adjacent to 203 | # a mutation operation 204 | MUTATION 205 | # Location adjacent to 206 | # a subscription operation 207 | SUBSCRIPTION 208 | # Location adjacent to 209 | # a field 210 | FIELD 211 | # Location adjacent to 212 | # a fragment definition 213 | FRAGMENT_DEFINITION 214 | # Location adjacent to 215 | # a fragment spread 216 | FRAGMENT_SPREAD 217 | # Location adjacent to 218 | # an inline fragment 219 | INLINE_FRAGMENT 220 | # Location adjacent to 221 | # a schema definition 222 | SCHEMA 223 | # Location adjacent to 224 | # a scalar definition 225 | SCALAR 226 | # Location adjacent to 227 | # an object type definition 228 | OBJECT 229 | # Location adjacent to 230 | # a field definition 231 | FIELD_DEFINITION 232 | # Location adjacent to 233 | # an argument definition 234 | ARGUMENT_DEFINITION 235 | # Location adjacent to 236 | # an interface definition 237 | INTERFACE 238 | # Location adjacent to 239 | # a union definition 240 | UNION 241 | # Location adjacent to 242 | # an enum definition 243 | ENUM 244 | # Location adjacent to 245 | # an enum value definition 246 | ENUM_VALUE 247 | # Location adjacent to 248 | # an input object type definition 249 | INPUT_OBJECT 250 | # Location adjacent to 251 | # an input object field definition 252 | INPUT_FIELD_DEFINITION 253 | } 254 | schema 255 | end 256 | end 257 | 258 | module Language 259 | class GraphQL::Language::TypeDefinition 260 | field :kind { nil } 261 | field :name 262 | field :description 263 | field :inputFields { nil } 264 | field :fields { nil } 265 | field :interfaces { nil } 266 | field :possibleTypes { nil } 267 | field :enumValues { nil } # (includeDeprecated: Boolean = false) 268 | field :ofType { nil } 269 | field :isDeprecated { false } 270 | field :deprecationReason { nil } 271 | end 272 | 273 | class GraphQL::Language::ObjectTypeDefinition 274 | field :kind { "OBJECT" } 275 | field :fields do |args, context| 276 | _fields = (resolved_interfaces(context.schema).flat_map(&.fields) + fields) 277 | .reduce(Hash(String, FieldDefinition).new) do |dict, field| 278 | dict[field.name] = field 279 | dict 280 | end.values.sort_by &.name 281 | if args["includeDeprecated"] 282 | _fields 283 | else 284 | _fields.reject(&.directives.any?(&.name.==("deprecated"))) 285 | end 286 | end 287 | field :interfaces { |args, context| resolved_interfaces(context.schema) } 288 | 289 | def resolved_interfaces(schema) 290 | interfaces.map do |iface_name| 291 | schema.type_resolve(iface_name).as(InterfaceTypeDefinition) 292 | end 293 | end 294 | end 295 | 296 | class GraphQL::Language::UnionTypeDefinition 297 | field :kind { "UNION" } 298 | field :possibleTypes { |args, context| types.map { |t| context.schema.type_resolve(t) } } 299 | end 300 | 301 | class GraphQL::Language::InterfaceTypeDefinition 302 | field :kind { "INTERFACE" } 303 | field :possibleTypes do |args, context| 304 | context.schema.types.values.select do |t| 305 | t.is_a?(ObjectTypeDefinition) && t.interfaces.includes?(self.name) 306 | end 307 | end 308 | field :fields 309 | end 310 | 311 | class GraphQL::Language::EnumTypeDefinition 312 | field :kind { "ENUM" } 313 | field :enumValues { self.fvalues } # (includeDeprecated: Boolean = false) 314 | end 315 | 316 | class GraphQL::Language::WrapperType 317 | field :name { nil } 318 | field :ofType { |args, context| context.schema.type_resolve(of_type) } 319 | end 320 | 321 | class GraphQL::Language::ListType 322 | field :kind { "LIST" } 323 | end 324 | 325 | class GraphQL::Language::NonNullType 326 | field :kind { "NON_NULL" } 327 | end 328 | 329 | class GraphQL::Language::ScalarTypeDefinition 330 | field :kind { "SCALAR" } 331 | end 332 | 333 | class GraphQL::Language::FieldDefinition 334 | include GraphQL::Directives::IsDeprecated 335 | field :name 336 | field :description 337 | field :args { self.arguments } 338 | field :type { |args, context| context.schema.type_resolve(type) } 339 | end 340 | 341 | class GraphQL::Language::InputObjectTypeDefinition 342 | field :inputFields { fields } 343 | field :kind { "INPUT_OBJECT" } 344 | field :directives 345 | end 346 | 347 | class GraphQL::Language::InputValueDefinition 348 | field :name 349 | field :description 350 | field :type { |args, context| context.schema.type_resolve(type) } 351 | field :defaultValue do 352 | val = ( 353 | default_value.is_a?(Language::AbstractNode) ? GraphQL::Language::Generation.generate(default_value) : ( 354 | default_value.is_a?(String) ? # quote the string value 355 | %{"#{default_value}"} : default_value 356 | ) 357 | ) 358 | val == nil ? nil : val.to_s 359 | end 360 | end 361 | 362 | class GraphQL::Language::DirectiveDefinition 363 | field :name 364 | field :description 365 | field :args { arguments } 366 | field :locations 367 | field :onOperation { locations.includes? "OPERATION" } 368 | field :onFragment { locations.any? &.=~ /FRAGMENT/ } 369 | field :onField { locations.includes? "FIELD" } 370 | end 371 | 372 | class GraphQL::Language::EnumValueDefinition 373 | include GraphQL::Directives::IsDeprecated 374 | field :name 375 | field :description 376 | end 377 | end 378 | end 379 | -------------------------------------------------------------------------------- /src/graphql-crystal/schema/variable_resolver.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | module Schema 3 | # 4 | # Visitor to Traverse the Queries AST and replace 5 | # VariableIdentifiers with the variables provided 6 | # in params 7 | class VariableResolver 8 | def self.visit(query : Language::OperationDefinition, params) 9 | query.tap &.selections = visit(query.selections, params).map &.as(Language::AbstractNode) 10 | end 11 | 12 | def self.visit(fields : Array(Language::AbstractNode), params) 13 | fields.map { |field| visit field, params } 14 | end 15 | 16 | def self.visit(field : Language::Field, params) 17 | field.tap do |field| 18 | field.selections = visit(field.selections, params).map &.as(Language::Selection) 19 | field.arguments = visit(field.arguments, params).map &.as(Language::Argument) 20 | end 21 | end 22 | 23 | def self.visit(argument : Language::Argument, params) 24 | argument.tap &.value = visit(argument.value, params).as(Language::ArgumentValue) 25 | end 26 | 27 | def self.visit(variable : Language::VariableIdentifier, params) 28 | params[variable.name] 29 | end 30 | 31 | def self.visit(field, params) 32 | field 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/graphql-crystal/types/object_type.cr: -------------------------------------------------------------------------------- 1 | macro on_all_child_classes(&block) 2 | 3 | macro injection 4 | {{block && block.body}} 5 | end 6 | 7 | macro inject 8 | injection 9 | macro inherited 10 | injection 11 | end 12 | end 13 | 14 | inject 15 | end 16 | 17 | macro on_included_s(&block) 18 | {{ block.body.stringify.id }} 19 | end 20 | 21 | macro on_included 22 | on_included_s do 23 | on_all_child_classes do 24 | GRAPHQL_FIELDS = [] of Tuple(Symbol, String, Hash(String, String)?, String) 25 | end 26 | 27 | on_all_child_classes do 28 | 29 | macro field(name, &block) 30 | field(\\{{name}}, "", args, "") \\{% if block.is_a?(Block)%} \\{{block}}\\{%end%} 31 | end 32 | 33 | macro field(name, description, args, typename, &block) 34 | \\{% GRAPHQL_FIELDS << {name, description, args, typename} %} 35 | private def field_\\{{name.id}}(\\{{(block.is_a?(Block) && block.args.size > 0) ? block.args.first.id : args}}, \\{{((block.is_a?(Block) && block.args.size > 1) ? block.args[1].id : "context").id}}) 36 | \\{% if block.is_a?(Block) %} 37 | context.with_self(\\{{(block.is_a?(Block) && block.args.size > 0) ? block.args.first.id : args}}) do 38 | \\{{block.body}} 39 | end 40 | \\{% else %} 41 | \\{{name.id}} 42 | \\{% end %} 43 | end 44 | end 45 | end 46 | 47 | on_all_child_classes do 48 | field :__typename { self.graphql_type } 49 | end 50 | 51 | on_all_child_classes do 52 | macro finished 53 | # 54 | # resolve a named field on this object with query arguments and context 55 | # 56 | def resolve_field(name : String, arguments, context : ::GraphQL::Schema::Context) 57 | \\{% prev_def = @type.methods.find(&.name.==("resolve_field")) %} 58 | \\{% if !GRAPHQL_FIELDS.empty? %} 59 | case name 60 | \\{% for field in @type.constant("GRAPHQL_FIELDS") %} 61 | when "\\{{ field[0].id }}" #\\\\\{{@type}} 62 | field_\\{{field[0].id}}(arguments, context) 63 | \\{% end %} 64 | else 65 | \\{% if prev_def.is_a?(Def) %} 66 | \\{{prev_def.args.map(&.name).splat}} = name, arguments, context 67 | \\{{prev_def.body}} 68 | \\{% else %} 69 | super(name, arguments, context) 70 | \\{% end %} 71 | end 72 | \\{% else %} 73 | \\{% if prev_def.is_a?(Def) %} 74 | \\{{prev_def.args.map(&.name).splat}} = name, arguments, context 75 | \\{{prev_def.body}} 76 | \\{% else %} 77 | super(name, arguments, context) 78 | \\{% end %} 79 | \\{% end %} 80 | end 81 | end 82 | end 83 | 84 | end 85 | end 86 | 87 | macro def_graphql_type(extended = false) 88 | {% unless @type.methods.any? &.name.==("graphql_type") %} 89 | # 90 | # get the GraphQL name of this object. 91 | # defaults to the class name 92 | # 93 | def {{extended ? "self.".id : "".id}}graphql_type 94 | "{{@type.name.gsub(/^(.*::)/, "")}}" 95 | end 96 | {% end %} 97 | end 98 | 99 | module GraphQL 100 | # 101 | # module to be included or extended by Classes and Modules 102 | # to make them act as GraphQL Objects. Provides the 103 | # `field` Macro for defining GraphQL Type Fields. 104 | # 105 | # ```crystal 106 | # class MyType 107 | # getter :name 108 | # def initialize(@name : String, @email : String); end 109 | # 110 | # includes GraphQL::ObjectType 111 | # field :name # with no further arguments 112 | # # the field will resolve to 113 | # # the getter method of the 114 | # # same name 115 | # 116 | # field :email { @email } # a block can be provided 117 | # # to access instance vars 118 | # 119 | # # a block will be called with an arguments hash 120 | # # and the context of the graphql request 121 | # field :signature do |args, context| 122 | # "#{@name} - #{args['with_email']? ? @email : ""}" 123 | # end 124 | # 125 | # end 126 | # ``` 127 | # 128 | module ObjectType 129 | # 130 | # get the GraphQL name of this object. 131 | # defaults to the class name 132 | # 133 | def graphql_type 134 | {{@type.name.gsub(/^(.*::)/, "").stringify}} 135 | end 136 | 137 | # 138 | # setter 139 | # can be used to set GraphQL name of the 140 | # Object. Defaults to the class name. Is 141 | # used in introspection queries 142 | # 143 | macro graphql_type(name) 144 | def graphql_type 145 | {{name}} 146 | end 147 | end 148 | 149 | # 150 | # setter that takes a block. 151 | # can be used to set GraphQL name of the 152 | # Object. Defaults to the class name. Is 153 | # used in introspection queries. 154 | # 155 | macro graphql_type(&block) 156 | {% if block.is_a?(Block) %} 157 | def graphql_type 158 | {{block.body}} 159 | end 160 | {% end %} 161 | end 162 | 163 | # 164 | # This method gets called when a field is resolved 165 | # on this object. The method gets automatically created 166 | # for every ObjectType 167 | # 168 | def resolve_field(name, arguments, context) 169 | pp "field not defined", name, self.class 170 | raise "field #{name} is not defined for #{self.class.name}" 171 | end 172 | 173 | macro included 174 | on_included 175 | macro inherited 176 | on_included 177 | end 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /src/graphql-crystal/types/type_validation.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | # 3 | # A TypeValidation is used to validate a given input against a 4 | # TypeDefinition. 5 | # 6 | class TypeValidation 7 | @enum_values_cache = Hash(String, Array(String)?).new { |hash, key| hash[key] = nil } 8 | 9 | def initialize(@types : Hash(String, Language::TypeDefinition)); end 10 | 11 | # 12 | # Returns true if `value` corresponds to 13 | # `type_definition`. 14 | # 15 | def accepts?(type_definition : GraphQL::Language::AbstractNode, value) : Bool 16 | # Nillable by default .. 17 | if value == nil && !type_definition.is_a?(Language::NonNullType) 18 | return true 19 | end 20 | 21 | case type_definition 22 | when Language::EnumTypeDefinition 23 | if value.is_a?(Language::AEnum) || value.is_a?(String) 24 | @enum_values_cache[type_definition.name] ||= type_definition.fvalues.map(&.as(Language::EnumValueDefinition).name) 25 | value_name = value.is_a?(Language::AEnum) ? value.name : value 26 | @enum_values_cache[type_definition.name].not_nil!.includes? value_name 27 | else 28 | false 29 | end 30 | when Language::UnionTypeDefinition 31 | type_definition.types.any? { |_type| accepts?(_type, value) } 32 | when Language::NonNullType 33 | value != nil ? accepts?(type_definition.of_type, value) : false 34 | when Language::ListType 35 | if value.is_a?(Array) 36 | value.map { |v| accepts?(type_definition.of_type, v).as(Bool) }.all? { |r| !!r } 37 | else 38 | false 39 | end 40 | when Language::ScalarTypeDefinition 41 | case type_definition.name 42 | when "ID" 43 | value.is_a?(Int) || value.is_a?(String) 44 | when "Int" 45 | value.is_a?(Int) 46 | when "Float" 47 | value.is_a?(Number) 48 | when "String" 49 | value.is_a?(String) 50 | when "Boolean" 51 | value.is_a?(Bool) 52 | else 53 | false 54 | end 55 | when Language::InputObjectTypeDefinition 56 | _value = value.is_a?(Language::InputObject) ? value.to_h : value 57 | return false unless _value.is_a? Hash 58 | (type_definition.fields.map(&.name) + _value.keys).uniq.each do |key| 59 | return false unless field = type_definition.fields.find(&.name.==(key)) 60 | if _value.has_key?(field.name) 61 | return false unless accepts?(field.type, _value[field.name]) 62 | elsif field.default_value 63 | return false unless accepts?(field.type, field.default_value) 64 | else 65 | return accepts?(field.type, nil) 66 | end 67 | end 68 | return true 69 | when Language::TypeName 70 | accepts?(@types[type_definition.name], value) 71 | else 72 | false 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /src/graphql-crystal/version.cr: -------------------------------------------------------------------------------- 1 | module GraphQL 2 | VERSION = "0.1.5" 3 | end 4 | --------------------------------------------------------------------------------