├── .gitignore ├── .travis.yml ├── CNAME ├── LICENSE ├── META6.json ├── Makefile ├── NASA_Open_Source_Agreement_1.3 GSC-17798.pdf ├── README.md ├── _config.yml ├── doc └── GraphQL.md ├── eg ├── graphiql-schema-request-query ├── hello-graphiql.png ├── hello.cro.p6 ├── hello.pl ├── users.pl ├── users.schema ├── usersexample.md ├── usersschema.md └── usersschema.pl ├── lib ├── GraphQL.pm └── GraphQL │ ├── Actions.pm │ ├── Compare.pm │ ├── Execution.pm │ ├── Grammar.pm │ ├── GraphiQL.pm │ ├── Introspection.pm │ ├── Response.pm │ ├── Types.pm │ └── Validation.pm ├── logotype └── logo_32x32.png ├── t ├── 01-parse-bad.t ├── 01-parse.t ├── 02-schemaparse.t ├── 03-schemafull.t ├── 04-query.t ├── 05-objectresolver.t ├── 06-queries-with-args.t ├── 07-mutations.t ├── 08-schemaobjects.t ├── 09-errors.t ├── 10-validation.t └── 11-abstract-types.t └── xt └── 01-my-meta.t /.gitignore: -------------------------------------------------------------------------------- 1 | .precomp 2 | *~ 3 | \#*\# 4 | .\#* 5 | testquery* 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl6 2 | perl6: 3 | - latest 4 | install: 5 | - rakudobrew build zef 6 | - zef install . 7 | sudo: false 8 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | graphql.tilmes.org 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | See NASA_Open_Source_Agreement_1.3 GSC-17798.pdf for original and binding text. 2 | 3 | NASA OPEN SOURCE AGREEMENT VERSION 1.3 4 | 5 | THIS OPEN SOURCE AGREEMENT (“AGREEMENT”) DEFINES THE RIGHTS OF 6 | USE, REPRODUCTION, DISTRIBUTION, MODIFICATION AND 7 | REDISTRIBUTION OF CERTAIN COMPUTER SOFTWARE ORIGINALLY 8 | RELEASED BY THE UNITED STATES GOVERNMENT AS REPRESENTED BY 9 | THE GOVERNMENT AGENCY LISTED BELOW ("GOVERNMENT AGENCY"). 10 | THE UNITED STATES GOVERNMENT, AS REPRESENTED BY GOVERNMENT 11 | AGENCY, IS AN INTENDED THIRD-PARTY BENEFICIARY OF ALL 12 | SUBSEQUENT DISTRIBUTIONS OR REDISTRIBUTIONS OF THE SUBJECT 13 | SOFTWARE. ANYONE WHO USES, REPRODUCES, DISTRIBUTES, MODIFIES 14 | OR REDISTRIBUTES THE SUBJECT SOFTWARE, AS DEFINED HEREIN, OR 15 | ANY PART THEREOF, IS, BY THAT ACTION, ACCEPTING IN FULL THE 16 | RESPONSIBILITIES AND OBLIGATIONS CONTAINED IN THIS AGREEMENT. 17 | 18 | Government Agency: National Aeronautics and Space Administration 19 | Government Agency Original Software Designation: GSC-17798-1, Version 0.5 20 | Government Agency Original Software Title: Perl 6 GraphQL 21 | User Registration Requested. Please Visit: http://opensource.gsfc.nasa.gov 22 | Government Agency Point of Contact for Original Software: Enidia Santiago-Arce, SRA 23 | Alternate, (301) 286-8497 24 | 25 | 26 | 1. DEFINITIONS 27 | 28 | A. “Contributor” means Government Agency, as the developer of the 29 | Original Software, and any entity that makes a Modification. 30 | B. “Covered Patents” mean patent claims licensable by a Contributor that 31 | are necessarily infringed by the use or sale of its Modification alone or 32 | when combined with the Subject Software. 33 | C. “Display” means the showing of a copy of the Subject Software, 34 | either directly or by means of an image, or any other device. 35 | D. “Distribution” means conveyance or transfer of the Subject Software, 36 | regardless of means, to another. 37 | E. “Larger Work” means computer software that combines Subject 38 | Software, or portions thereof, with software separate from the Subject 39 | Software that is not governed by the terms of this Agreement. 40 | F. “Modification” means any alteration of, including addition to or 41 | deletion from, the substance or structure of either the Original 42 | Software or Subject Software, and includes derivative works, as that 43 | term is defined in the Copyright Statute, 17 USC 101. However, the 44 | act of including Subject Software as part of a Larger Work does not in 45 | and of itself constitute a Modification. 46 | G. “Original Software” means the computer software first released under 47 | this Agreement by Government Agency with Government Agency 48 | designation: GSC-17798-1, Version 0.5 and entitled Perl 6 GraphQL, 49 | 50 | 51 | 52 | 1 53 | including source code, object code and accompanying documentation, 54 | if any. 55 | H. “Recipient” means anyone who acquires the Subject Software under 56 | this Agreement, including all Contributors. 57 | I. “Redistribution” means Distribution of the Subject Software after a 58 | Modification has been made. 59 | J. “Reproduction” means the making of a counterpart, image or copy of 60 | the Subject Software. 61 | K. “Sale” means the exchange of the Subject Software for money or 62 | equivalent value. 63 | L. “Subject Software” means the Original Software, Modifications, or 64 | any respective parts thereof. 65 | M. “Use” means the application or employment of the Subject Software 66 | for any purpose. 67 | 68 | 2. GRANT OF RIGHTS 69 | 70 | A. Under Non-Patent Rights: Subject to the terms and conditions of this 71 | Agreement, each Contributor, with respect to its own contribution to 72 | the Subject Software, hereby grants to each Recipient a non-exclusive, 73 | world-wide, royalty-free license to engage in the following activities 74 | pertaining to the Subject Software: 75 | 76 | 1. Use 77 | 2. Distribution 78 | 3. Reproduction 79 | 4. Modification 80 | 5. Redistribution 81 | 6. Display 82 | 83 | B. Under Patent Rights: Subject to the terms and conditions of this 84 | Agreement, each Contributor, with respect to its own contribution to 85 | the Subject Software, hereby grants to each Recipient under Covered 86 | Patents a non-exclusive, world-wide, royalty-free license to engage in 87 | the following activities pertaining to the Subject Software: 88 | 89 | 1. Use 90 | 2. Distribution 91 | 3. Reproduction 92 | 4. Sale 93 | 5. Offer for Sale 94 | 95 | C. The rights granted under Paragraph B. also apply to the combination of 96 | a Contributor’s Modification and the Subject Software if, at the time 97 | the Modification is added by the Contributor, the addition of such 98 | Modification causes the combination to be covered by the Covered 99 | 100 | 101 | 102 | 2 103 | Patents. It does not apply to any other combinations that include a 104 | Modification. 105 | 106 | D. The rights granted in Paragraphs A. and B. allow the Recipient to 107 | sublicense those same rights. Such sublicense must be under the same 108 | terms and conditions of this Agreement. 109 | 110 | 3. OBLIGATIONS OF RECIPIENT 111 | 112 | A. Distribution or Redistribution of the Subject Software must be made 113 | under this Agreement except for additions covered under paragraph 114 | 3H. 115 | 116 | 1. Whenever a Recipient distributes or redistributes the Subject 117 | Software, a copy of this Agreement must be included with each 118 | copy of the Subject Software; and 119 | 2. If Recipient distributes or redistributes the Subject Software in 120 | any form other than source code, Recipient must also make the 121 | source code freely available, and must provide with each copy of 122 | the Subject Software information on how to obtain the source 123 | code in a reasonable manner on or through a medium 124 | customarily used for software exchange. 125 | 126 | B. Each Recipient must ensure that the following copyright notice 127 | appears prominently in the Subject Software: 128 | 129 | Copyright  2016 United States Government as represented by the 130 | Administrator of the National Aeronautics and Space Administration. 131 | No copyright is claimed in the United States under Title 17, U.S.Code. 132 | All Other Rights Reserved. 133 | 134 | C. Each Contributor must characterize its alteration of the Subject 135 | Software as a Modification and must identify itself as the originator of 136 | its Modification in a manner that reasonably allows subsequent 137 | Recipients to identify the originator of the Modification. In fulfillment 138 | of these requirements, Contributor must include a file (e.g., a change 139 | log file) that describes the alterations made and the date of the 140 | alterations, identifies Contributor as originator of the alterations, and 141 | consents to characterization of the alterations as a Modification, for 142 | example, by including a statement that the Modification is derived, 143 | directly or indirectly, from Original Software provided by Government 144 | Agency. Once consent is granted, it may not thereafter be revoked. 145 | 146 | D. A Contributor may add its own copyright notice to the Subject 147 | Software. Once a copyright notice has been added to the Subject 148 | 149 | 150 | 151 | 152 | 3 153 | Software, a Recipient may not remove it without the express 154 | permission of the Contributor who added the notice. 155 | 156 | E. A Recipient may not make any representation in the Subject Software 157 | or in any promotional, advertising or other material that may be 158 | construed as an endorsement by Government Agency or by any prior 159 | Recipient of any product or service provided by Recipient, or that may 160 | seek to obtain commercial advantage by the fact of Government 161 | Agency's or a prior Recipient’s participation in this Agreement. 162 | 163 | F. In an effort to track usage and maintain accurate records of the Subject 164 | Software, each Recipient, upon receipt of the Subject Software, is 165 | requested to register with Government Agency by visiting the 166 | following website: http://opensource.gsfc.nasa.gov. Recipient’s name 167 | and personal information shall be used for statistical purposes only. 168 | Once a Recipient makes a Modification available, it is requested that 169 | the Recipient inform Government Agency at the web site provided 170 | above how to access the Modification. 171 | 172 | G. Each Contributor represents that that its Modification is believed to be 173 | Contributor’s original creation and does not violate any existing 174 | agreements, regulations, statutes or rules, and further that Contributor 175 | has sufficient rights to grant the rights conveyed by this Agreement. 176 | 177 | H. A Recipient may choose to offer, and to charge a fee for, warranty, 178 | support, indemnity and/or liability obligations to one or more other 179 | Recipients of the Subject Software. A Recipient may do so, however, 180 | only on its own behalf and not on behalf of Government Agency or 181 | any other Recipient. Such a Recipient must make it absolutely clear 182 | that any such warranty, support, indemnity and/or liability obligation 183 | is offered by that Recipient alone. Further, such Recipient agrees to 184 | indemnify Government Agency and every other Recipient for any 185 | liability incurred by them as a result of warranty, support, indemnity 186 | and/or liability offered by such Recipient. 187 | 188 | I. A Recipient may create a Larger Work by combining Subject Software 189 | with separate software not governed by the terms of this agreement 190 | and distribute the Larger Work as a single product. In such case, the 191 | Recipient must make sure Subject Software, or portions thereof, 192 | included in the Larger Work is subject to this Agreement. 193 | 194 | J. Notwithstanding any provisions contained herein, Recipient is hereby 195 | put on notice that export of any goods or technical data from the 196 | United States may require some form of export license from the U.S. 197 | Government. Failure to obtain necessary export licenses may result in 198 | criminal liability under U.S. laws. Government Agency neither 199 | 200 | 201 | 202 | 4 203 | represents that a license shall not be required nor that, if required, it 204 | shall be issued. Nothing granted herein provides any such export 205 | license. 206 | 207 | 4. DISCLAIMER OF WARRANTIES AND LIABILITIES; WAIVER AND 208 | INDEMNIFICATION 209 | 210 | A. No Warranty: THE SUBJECT SOFTWARE IS PROVIDED “AS IS” 211 | WITHOUT ANY WARRANTY OF ANY KIND, EITHER 212 | EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT 213 | NOT LIMITED TO, ANY WARRANTY THAT THE SUBJECT 214 | SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY 215 | IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS 216 | FOR A PARTICULAR PURPOSE, OR FREEDOM FROM 217 | INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT 218 | SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY 219 | THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO 220 | THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN 221 | ANY MANNER, CONSTITUTE AN ENDORSEMENT BY 222 | GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY 223 | RESULTS, RESULTING DESIGNS, HARDWARE, SOFTWARE 224 | PRODUCTS OR ANY OTHER APPLICATIONS RESULTING 225 | FROM USE OF THE SUBJECT SOFTWARE. FURTHER, 226 | GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND 227 | LIABILITIES REGARDING THIRD-PARTY SOFTWARE, IF 228 | PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES 229 | IT “AS IS.” 230 | 231 | B. Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY 232 | AND ALL CLAIMS AGAINST THE UNITED STATES 233 | GOVERNMENT, ITS CONTRACTORS AND 234 | SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF 235 | RECIPIENT'S USE OF THE SUBJECT SOFTWARE RESULTS IN 236 | ANY LIABILITIES, DEMANDS, DAMAGES, EXPENSES OR 237 | LOSSES ARISING FROM SUCH USE, INCLUDING ANY 238 | DAMAGES FROM PRODUCTS BASED ON, OR RESULTING 239 | FROM, RECIPIENT'S USE OF THE SUBJECT SOFTWARE, 240 | RECIPIENT SHALL INDEMNIFY AND HOLD HARMLESS THE 241 | UNITED STATES GOVERNMENT, ITS CONTRACTORS AND 242 | SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT, TO 243 | THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE 244 | REMEDY FOR ANY SUCH MATTER SHALL BE THE 245 | IMMEDIATE, UNILATERAL TERMINATION OF THIS 246 | AGREEMENT. 247 | 248 | 249 | 250 | 251 | 5 252 | 5. GENERAL TERMS 253 | 254 | A. Termination: This Agreement and the rights granted hereunder will 255 | terminate automatically if a Recipient fails to comply with these terms 256 | and conditions, and fails to cure such noncompliance within thirty (30) 257 | days of becoming aware of such noncompliance. Upon termination, a 258 | Recipient agrees to immediately cease use and distribution of the 259 | Subject Software. All sublicenses to the Subject Software properly 260 | granted by the breaching Recipient shall survive any such termination 261 | of this Agreement. 262 | 263 | B. Severability: If any provision of this Agreement is invalid or 264 | unenforceable under applicable law, it shall not affect the validity or 265 | enforceability of the remainder of the terms of this Agreement. 266 | 267 | C. Applicable Law: This Agreement shall be subject to United States 268 | federal law only for all purposes, including, but not limited to, 269 | determining the validity of this Agreement, the meaning of its 270 | provisions and the rights, obligations and remedies of the parties. 271 | 272 | D. Entire Understanding: This Agreement constitutes the entire 273 | understanding and agreement of the parties relating to release of the 274 | Subject Software and may not be superseded, modified or amended 275 | except by further written agreement duly executed by the parties. 276 | 277 | E. Binding Authority: By accepting and using the Subject Software 278 | under this Agreement, a Recipient affirms its authority to bind the 279 | Recipient to all terms and conditions of this Agreement and that that 280 | Recipient hereby agrees to all terms and conditions herein. 281 | 282 | F. Point of Contact: Any Recipient contact with Government Agency is 283 | to be directed to the designated representative as follows: Enidia 284 | Santiago-Arce, SRA Alternate, (301) 286-8497 285 | 286 | 287 | 288 | 289 | 6 290 | 291 | -------------------------------------------------------------------------------- /META6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "GraphQL", 3 | 4 | "version" : "0.6.2", 5 | 6 | "source-url" : "https://github.com/CurtTilmes/Perl6-GraphQL.git", 7 | 8 | "perl" : "6.*", 9 | 10 | "tags" : [ "GraphQL" ], 11 | 12 | "provides" : { 13 | "GraphQL" : "lib/GraphQL.pm", 14 | "GraphQL::Actions" : "lib/GraphQL/Actions.pm", 15 | "GraphQL::Compare" : "lib/GraphQL/Compare.pm", 16 | "GraphQL::Execution" : "lib/GraphQL/Execution.pm", 17 | "GraphQL::Grammar" : "lib/GraphQL/Grammar.pm", 18 | "GraphQL::GraphiQL" : "lib/GraphQL/GraphiQL.pm", 19 | "GraphQL::Introspection" : "lib/GraphQL/Introspection.pm", 20 | "GraphQL::Response" : "lib/GraphQL/Response.pm", 21 | "GraphQL::Types" : "lib/GraphQL/Types.pm", 22 | "GraphQL::Validation" : "lib/GraphQL/Validation.pm" 23 | }, 24 | 25 | "depends" : [ "JSON::Fast", "Text::Wrap:auth" ], 26 | 27 | "description" : "Perl6 implementation of GraphQL", 28 | 29 | "test-depends" : [ "Test" ], 30 | 31 | "license" : "NASA-1.3", 32 | 33 | "authors" : [ "Curt Tilmes " ], 34 | 35 | "support" : { 36 | "source" : "https://github.com/CurtTilmes/Perl6-GraphQL.git", 37 | "bugtracker" : "https://github.com/CurtTilmes/Perl6-GraphQL/issues" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := $(shell jq -r .name META6.json) 2 | VERSION := $(shell jq -r .version META6.json) 3 | ARCHIVENAME := $(subst ::,-,$(NAME)) 4 | 5 | check: 6 | git diff-index --check HEAD 7 | prove6 8 | 9 | tag: 10 | git tag $(VERSION) 11 | git push origin --tags 12 | 13 | dist: 14 | git archive --prefix=$(ARCHIVENAME)-$(VERSION)/ \ 15 | -o ../$(ARCHIVENAME)-$(VERSION).tar.gz $(VERSION) 16 | -------------------------------------------------------------------------------- /NASA_Open_Source_Agreement_1.3 GSC-17798.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CurtTilmes/Perl6-GraphQL/b05d61c53f52502045609e79d8c01036a65e0ef7/NASA_Open_Source_Agreement_1.3 GSC-17798.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Perl 6 GraphQL 2 | ============== 3 | 4 | [![Build Status](https://travis-ci.org/CurtTilmes/Perl6-GraphQL.svg)](https://travis-ci.org/CurtTilmes/Perl6-GraphQL) 5 | 6 | A [Perl 6](https://perl6.org/) implementation of the 7 | [GraphQL](http://graphql.org/) standard. GraphQL is a query language 8 | for APIs originally created by Facebook. 9 | 10 | ## Intro 11 | 12 | Before we get into all the details, here's the Perl 6 GraphQL "Hello World" 13 | [hello.pl](https://github.com/CurtTilmes/Perl6-GraphQL/blob/master/eg/hello.pl) 14 | 15 | 16 | ``` 17 | use GraphQL; 18 | use GraphQL::Server; 19 | 20 | class Query 21 | { 22 | method hello(--> Str) { 'Hello World' } 23 | } 24 | 25 | my $schema = GraphQL::Schema.new(Query); 26 | 27 | GraphQL-Server($schema); 28 | 29 | ``` 30 | 31 | You can run this with a GraphQL query on the command line: 32 | ``` 33 | $ perl6 hello.pl --help 34 | Usage: 35 | hello.pl 36 | hello.pl [--filename=] 37 | hello.pl [--port=] 38 | 39 | $ perl6 hello.pl '{hello}' 40 | { 41 | "data": { 42 | "hello": "Hello World" 43 | } 44 | } 45 | ``` 46 | 47 | You can even ask for information about the schema and types: 48 | ``` 49 | $ perl6 hello.pl '{ __schema { queryType { name } } }' 50 | { 51 | "data": { 52 | "__schema": { 53 | "queryType": { 54 | "name": "Query" 55 | } 56 | } 57 | } 58 | } 59 | 60 | $ perl6 hello.pl '{ __type(name: "Query") { fields { name type { name }}}}' 61 | { 62 | "data": { 63 | "__type": { 64 | "fields": [ 65 | { 66 | "name": "hello", 67 | "type": { 68 | "name": "String" 69 | } 70 | } 71 | ] 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ## Running as a server 78 | 79 | That's fine for the command line, but you can also easily wrap GraphQL 80 | into a web server to expose that API to external clients. GraphQL::Server 81 | uses the Perl 6 web framework 82 | [Bailador](https://github.com/ufobat/Bailador) to do that: 83 | 84 | ``` 85 | $ ./hello.pl 86 | Entering the development dance floor: http://0.0.0.0:3000 87 | [2016-12-21T13:02:38Z] Started HTTP server. 88 | 89 | ``` 90 | 91 | The server takes any GraphQL query sent with HTTP POST to /graphql, 92 | executes it against the GraphQL Schema, and returns the result in 93 | JSON. 94 | 95 | There is one additional feature. If it receives a GET request to 96 | "/graphql", it sends back the 97 | [GraphiQL](https://github.com/graphql/graphiql) graphical interactive 98 | in-browser GraphQL IDE. 99 | 100 | ![](eg/hello-graphiql.png) 101 | 102 | You can use that to explore the schema (though the Hello World schema 103 | is very simple, that won't take long), and interactively construct and 104 | execute GraphQL queries. 105 | 106 | ## Embedding in a Cro server 107 | 108 | As an alternative to Bailador, you can use `Cro::HTTP::Router::GraphQL` 109 | to embed GraphQL into [Cro](http://mi.cro.services/) HTTP routes: 110 | 111 | ``` 112 | use GraphQL; 113 | use Cro::HTTP::Router::GraphQL; 114 | use Cro::HTTP::Router; 115 | use Cro::HTTP::Server; 116 | 117 | class Query 118 | { 119 | method hello(--> Str) { 'Hello World' } 120 | } 121 | 122 | my $schema = GraphQL::Schema.new(Query); 123 | 124 | my Cro::Service $hello = Cro::HTTP::Server.new: 125 | :host, :port<10000>, 126 | application => route 127 | { 128 | get -> { redirect '/graphql' } 129 | 130 | get -> 'graphql' { graphiql } 131 | 132 | post -> 'graphql' { graphql($schema) } 133 | } 134 | 135 | $hello.start; 136 | 137 | react whenever signal(SIGINT) { $hello.stop; exit; } 138 | ``` 139 | 140 | You can mix/match with other routes you want your server to handle. 141 | 142 | There is also a `CroX::HTTP::Transform::GraphQL` you can easily delegate 143 | to from Cro routes: 144 | 145 | ``` 146 | route { 147 | delegate graphql => CroX::HTTP::Transform::GraphQL.new(:$schema, :graphiql); 148 | } 149 | ``` 150 | 151 | Pass in your GraphQL schema, and optional `:graphiql` to enable 152 | GraphiQL support on an http GET. 153 | 154 | `CroX::HTTP::Transform::GraphQL` is a `Cro::HTTP::Transform` that 155 | consumes `Cro::HTTP::Request`s and produces `Cro::HTTP::Response`s. 156 | It is still pretty basic. A planned enhancement is caching parsed 157 | GraphQL query documents. (Patches or advice welcome!) 158 | 159 | ## More documentation 160 | 161 | See [eg/usersexample.md](/eg/usersexample.md) for a more complicated example. 162 | 163 | See [slides](https://curttilmes.github.com/2017-GraphQL-PHLPM) from a 164 | presentation about Perl 6 GraphQL at the Philadelphia Perl Mongers. 165 | 166 | [GraphQL Documentation](/doc/GraphQL.md) 167 | 168 | Copyright © 2017 United States Government as represented by the 169 | Administrator of the National Aeronautics and Space Administration. 170 | No copyright is claimed in the United States under Title 17, 171 | U.S.Code. All Other Rights Reserved. 172 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile 2 | -------------------------------------------------------------------------------- /doc/GraphQL.md: -------------------------------------------------------------------------------- 1 | GraphQL 2 | ======= 3 | 4 | SYNOPSIS 5 | -------- 6 | 7 | use GraphQL; 8 | 9 | class Query 10 | { 11 | method hello(--> Str) { 'Hello World' } 12 | } 13 | 14 | my $schema = GraphQL::Schema.new(Query); 15 | 16 | say $schema.execute('{ hello }').to-json; 17 | 18 | DESCRIPTION 19 | ----------- 20 | 21 | "GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools." - Facebook Inc., [**http://graphql.org**](**http://graphql.org**). 22 | 23 | The GraphQL Language is described in detail at [**http://graphql.org**](**http://graphql.org**) which also includes the draft specification. This module is a Perl 6 server implementation of that specification (or will be once it is complete). The intent of this documentation isn't to fully describe GraphQL and its usage, but rather to describe that Perl implementation and how various functionality is accessible through Perl. This document will assume basic awareness of GraphQL and that standard. 24 | 25 | OVERVIEW 26 | -------- 27 | 28 | GraphQL itself isn't a database, it is the interface between the client and whatever database or other data store you use. Constructing a GraphQL server consists of describing your API **Schema** consisting of a data structure of data **Types**, and connecting to subroutines or methods for **Resolution** of the actual data values. The **Schema** is the controller or orchestrator for everything. It performs two major functions, **Validation** to determine if a query is valid at all, and **Execution**, which makes calls to arbitrary code for **Resolution** to determine the resulting data structure. The GraphQL language also specifies **Introspection** which is essentially **Resolution** carried out by the Schema itself to describe itself. 29 | 30 | The synopsis above describes the simplest GraphQL server possible. It consists of a single Type or Class called **Query**, with a single field in it called *hello* of type String, with a method attached to it that returns the string `Hello World`. 31 | 32 | The schema is constructed by passing the Perl 6 class into the `GraphQL::Schema`'s `new()` constructor. The example then passes in the simplest GraphQL query {hello}. Execution will call the `hello()` method and return the result in a `GraphQL::Result` structure that can then be converted into JSON with `to-json()` method which will return the result: 33 | 34 | { 35 | "data": { 36 | "hello": "Hello World" 37 | } 38 | } 39 | 40 | In a typical GraphQL Web server, the query would be HTTP POSTed to an endpoint at `/graphql` which would call `GraphQL::Schema.execute()` and send the resulting JSON string back to the requester. 41 | 42 | Each of those steps will be described in more detail below. 43 | 44 | Schema Styles 45 | ------------- 46 | 47 | This module currently supports three different *styles* for expressing GraphQL types for your GraphQL schema: 48 | 49 | * **Manual** - You can construct each type by creating and nesting various `GraphQL::*` objects. 50 | 51 | For the "Hello World" example, it would look like this: 52 | 53 | my $schema = GraphQL::Schema.new( 54 | GraphQL::Object.new( 55 | name => 'Query', 56 | fieldlist => GraphQL::Field.new( 57 | name => 'hello', 58 | type => $GraphQLString, 59 | resolver => sub { 'Hello World' } 60 | ) 61 | ) 62 | ); 63 | 64 | * **GraphQL Schema Language** or **GSL**- The Perl 6 GraphQL engine includes a complete parser for the *GraphQL Schema Language* described in detail at [**http://graphql.org**](**http://graphql.org**). It is important to note that this is a _different_ language from the *GraphQL Query language* which will be described later. There is also a handy cheat sheet for the *GSL* at [https://github.com/sogko/graphql-shorthand-notation-cheat-sheet/](https://github.com/sogko/graphql-shorthand-notation-cheat-sheet/). 65 | 66 | For the "Hello World" example, it would look like this: 67 | 68 | my $schema = GraphQL::Schema.new('type Query { hello: String }', 69 | resolvers => { Query => { hello => sub { 'Hello World' } } }); 70 | 71 | Note that while the schema type descriptions are provided in the *GSL*, the resolving functions for each field must be separately supplied in a two level hash with the names of each Object Type at the first level, and Field at the second level. 72 | 73 | * **Direct Perl Classes** - You can also simply pass in Perl 6 classes directly. A matching schema is constructed by examining the classes with the Perl language Metamodel for introspection. Given the GraphQL type restrictions, not everything you can express in Perl will result in a valid Schema, so it is important to use only the types as described below. Also restrict the names of attributes and methods to the alpha-numeric and '_'. (No fancy unicode names, or kebab-case names.) 74 | 75 | For the "Hello World" example, it looks like this: 76 | 77 | class Query 78 | { 79 | method hello(--> Str) { 'Hello World' } 80 | } 81 | 82 | my $schema = GraphQL::Schema.new(Query); 83 | 84 | Under the hood, the Schemas all look the same, regardless of which style you use to construct them. The later two options are just additional syntactic sugar to make things easier. You can also mix and match, making some types one way and some another and everything will still work fine. 85 | 86 | Types 87 | ----- 88 | 89 | GraphQL is a strongly, staticly typed language. Every type must be defined precisely up front, and all can be checked during validation phase prior to execution. 90 | 91 | The Perl Class hierarchy for GraphQL Types includes these: 92 | 93 | * **GraphQL::Type** (abstract, not to be used directly, only inherited 94 | 95 | * **GraphQL::Scalar** 96 | 97 | * **GraphQL::String** 98 | 99 | * **GraphQL::Int** 100 | 101 | * **GraphQL::Float** 102 | 103 | * **GraphQL::ID** 104 | 105 | * **GraphQL::EnumValue** 106 | 107 | * **GraphQL::List** 108 | 109 | * **GraphQL::Non-Null** 110 | 111 | * **GraphQL::InputValue** 112 | 113 | * **GraphQL::Field** 114 | 115 | * **GraphQL::Interface** 116 | 117 | * **GraphQL::Object** 118 | 119 | * **GraphQL::InputObjectType** 120 | 121 | * **GraphQL::Union** 122 | 123 | * **GraphQL::Enum** 124 | 125 | * **GraphQL::Directive** 126 | 127 | ### *role* **Deprecatable** 128 | 129 | **GraphQL::Field** and **GraphQL::EnumValue** are **Deprecatable** 130 | 131 | They get two extra public attributes `$.isDeprecated` *Bool*, default `False`, and `$.deprecationReason` *Str*. 132 | 133 | They also get the method `.deprecate(Str $reason)`, which defaults to "No longer supported." 134 | 135 | In *GSL*, you can also deprecate with the directive **@deprecate** or `@deprecate(reason: "something")`. More on directives below. 136 | 137 | ### *role* **HasFields** 138 | 139 | **GraphQL::Object** and **GraphQL::Interface** both include a role **HasFields** that give them a **@.fieldlist** array of **GraphQL::Field**s, a method **.field($name)** to look up a field, and a method **.fields(Bool :$includeDeprecated)** that will return the list of fields. Meta-fields with names starting with "__" are explicitly not returned in the `.fields()` list, but can be requested with `.field()`. 140 | 141 | ### **GraphQL::Type** 142 | 143 | This is the main GraphQL type base class. It has public attributes `$.name` and `$.description`. It isn't intended to be used directly, it is just the base class for all the other Types. 144 | 145 | The description field can be explicitly assigned in the creation of each GraphQL::Type. 146 | 147 | In *GSL*, you can set the description field by preceding the definition of types with comments: 148 | 149 | # Description for mytype 150 | type mytype { 151 | # Description for myfield 152 | myfield: Str 153 | } 154 | 155 | In Perl, the description field is set from the Meto-Object Protocol $obj.WHY method which by default will be set automatically with Pod declarations. e.g. 156 | 157 | #| Description for mytype 158 | class mytype { 159 | #| Description for myfield 160 | has Str $.myfield 161 | } 162 | 163 | ### **GraphQL::Scalar** is **GraphQL::Type** 164 | 165 | Serves as the base class for scalar, leaf types. It adds the method **.kind()** = 'SCALAR'; 166 | 167 | There are several core GraphQL scalar types that map to Perl basic scalar types: 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 |
GraphQL TypePerl Type ClassPerl Object InstancePerl Type
StringGraphQL::String$GraphQLStringStr
IntGraphQL::Int$GraphQLIntInt
FloatGraphQL::Float$GraphQLFloatNum
BooleanGraphQL::Boolean$GraphQLBooleanBool
IDGraphQL::ID$GraphQLIDID (subset of Cool)
209 | 210 | The Perl Object Instances are just short hand pre-created objects that can be used since those types are needed so frequently. 211 | 212 | For example, GraphQL::String.new creates a String type, but you can just use $GraphQLString which is already made. 213 | 214 | You can create your own additional scalar types as needed: 215 | 216 | my $URL = GraphQL::Scalar.new(name => 'URL'); 217 | 218 | or in *GSL*: 219 | 220 | scalar URL 221 | 222 | #### **GraphQL::String** is **GraphQL::Scalar** 223 | 224 | Core String type, maps to Perl type `Str`. 225 | 226 | You can create your own: 227 | 228 | my $String = GraphQL::String.new; 229 | 230 | or just use `$GraphQLString`. 231 | 232 | #### **GraphQL::Int** is **GraphQL::Scalar** 233 | 234 | Core Int type, maps to Perl type `Int`. 235 | 236 | You can create your own: 237 | 238 | my $Int = GraphQL::Int.new; 239 | 240 | or just use `$GraphQLInt`. 241 | 242 | #### **GraphQL::Float** is **GraphQL::Scalar** 243 | 244 | Core Float type, maps to Perl type `Num`. 245 | 246 | You can create your own: 247 | 248 | my $Float = GraphQL::Float.new; 249 | 250 | or just use `$GraphQLFloat`. 251 | 252 | #### **GraphQL::Boolean** is **GraphQL::Scalar** 253 | 254 | Core Boolean type, maps to Perl type `Bool`. 255 | 256 | You can create your own: 257 | 258 | my $Boolean = GraphQL::Boolean.new; 259 | 260 | or just use `$GraphQLBoolean`. 261 | 262 | #### **GraphQL::ID** is **GraphQL::Scalar** 263 | 264 | Core ID type, maps to Perl type `ID` which is a subset of `Cool`. 265 | 266 | You can create your own: 267 | 268 | my $ID = GraphQL::ID.new; 269 | 270 | or just use `$GraphQLID`. 271 | 272 | #### **GraphQL::EnumValue** is **GraphQL::Scalar** does **Deprecatable** 273 | 274 | The individual enumerated values of an `Enum`, represented as quoted strings in JSON. 275 | 276 | my $enumvalue = GraphQL::EnumValue.new(name => 'SOME_VALUE'); 277 | 278 | They can also be deprecated: 279 | 280 | my $enumvalue = GraphQL::EnumValue.new(name => 'SOME_VALUE', 281 | :isDeprecated, 282 | reason => 'Just because'); 283 | 284 | or can be later deprecated: 285 | 286 | $enumvalue.deprecate('Just because'); 287 | 288 | See **GraphQL::Enum** for more information about creating EnumValues. 289 | 290 | #### **GraphQL::List** is **GraphQL::Type** 291 | 292 | **.kind()** = 'LIST', and has **$.ofType** with some other GraphQL::Type. 293 | 294 | my $list-of-strings = GraphQL::List.new(ofType => $GraphQLString); 295 | 296 | In *GSL*, Lists are represented by wrapping another type with square brackets '[' and ']'. e.g. 297 | 298 | [String] 299 | 300 | #### **GraphQL::Non-Null** is **GraphQL::Type** 301 | 302 | By default GraphQL types can all take on the value `null` (in Perl, `Nil`). Wrapping them with Non-Null disallows the `null`. 303 | 304 | **.kind()** = 'NON_NULL' 305 | 306 | my $non-null-string = GraphQL::Non-Null.new(ofType => $GraphQLString); 307 | 308 | In *GSL*, Non-Null types are represented by appending an exclation point, '!'. e.g. 309 | 310 | String! 311 | 312 | To define a Perl class with a non-null attribute, both add the `:D` type constraint to the type, and also specify it as `is required` (or give it a default). To mark a type in a method as non-null, append with an exclamation point. e.g. 313 | 314 | class Something 315 | { 316 | has Str:D $.my is rw is required; 317 | 318 | method something(Str :$somearg! --> ID) { ... } 319 | } 320 | 321 | #### **GraphQL::InputValue** is **GraphQL::Type** 322 | 323 | The type is used to represent arguments for **GraphQL::Field**s and **Directive**s arguments as well as the `inputFields` of a **GraphQL::InputObjectType**. Has a `$.type` attribute and optionally a `$.defaultValue` attribute. 324 | 325 | my $inputvalue = GraphQL::InputValue.new(name => 'somearg', 326 | type => $GraphQLString, 327 | defaultValue => 'some default'); 328 | 329 | in *GSL*: 330 | 331 | somearg: String = "some default" 332 | 333 | in Perl: 334 | 335 | Str :$somearg = 'some default' 336 | 337 | #### **GraphQL::Field** is **GraphQL::Type** does **Deprecatable** 338 | 339 | In addition to the inherited **.name**, **.description**, **.isDeprecated**, **.deprecationReason**, has attributes **.args** which is an array of **GraphQL::InputValue**s, and **.type** which is the type of this field. Since the Field is the place where the Schema connects to resolvers, there is also a **.resolver** attribute which can be connected to arbitrary code. Much more about resolvers in Resolution below. 340 | 341 | my $field = GraphQL::Field.new( 342 | name => 'myfield', 343 | type => $GraphQLString, 344 | args => GraphQL::InputValue.new( 345 | name => 'somearg', 346 | type => $GraphQLString, 347 | defaultValue => 'some default'), 348 | resolver => sub { ... }); 349 | 350 | In *GSL*: 351 | 352 | myfield(somearg: String = "some default"): String 353 | 354 | In Perl: 355 | 356 | method myfield(Str :$somearg = 'some default' --> Str) { ... } 357 | 358 | Note that as a strongly, staticly typed system, every argument must be a named argument, and have an attached type (a valid one in the list above that map to GraphQL types), and the return must specify a type. 359 | 360 | You can deprecate by setting the attributes **.isDeprecated** and optionally **.deprecationReason** or using the *GSL* **@deprecate** directive described below. 361 | 362 | #### **GraphQL::Interface** is **GraphQL::Type** does **HasFields** 363 | 364 | In addition to the inherited **$.name**, **$.description**, and **@.fieldlist**, also has the attribute **@.possibleTypes** with the list of object types that implement the interface. You needn't set **@.possibleTypes**, as each **GraphQL::Object** specifies which interfaces they implement, and the Schema finalization will list them all here. 365 | 366 | my $interface = GraphQL::Interface.new( 367 | name => 'myinterface', 368 | fieldlist => (GraphQL::Field.new(...), GraphQL::Field.new(...)) 369 | ); 370 | 371 | In *GSL*: 372 | 373 | interface myinterface { 374 | ...fields... 375 | } 376 | 377 | #### **GraphQL::Object** is **GraphQL::Type** does **HasFields** 378 | 379 | In addition to the inherited **$.name**, **$.description**, and **@.fieldlist**, also has the attribute **@.interfaces** with the interfaces which the object implements, and the **.kind()** method which always returns 'OBJECT'. 380 | 381 | my $obj = GraphQL::Object.new( 382 | name => 'myobject', 383 | interfaces => ($someinterface, $someotherinterface), 384 | fieldlist => (GraphQL::Field.new(...), GraphQL::Field.new(...)) 385 | ); 386 | 387 | In *GSL*: 388 | 389 | type myobject implements someinterface, someotherinterface { 390 | ...fields... 391 | } 392 | 393 | In Perl: 394 | 395 | class myobject { 396 | ...fields... 397 | } 398 | 399 | NOTE: Interfaces aren't yet implemented for the perl classes. 400 | 401 | #### **GraphQL::InputObjectType** is **GraphQL::Type** 402 | 403 | Input Objects are object like types used as inputs to queries. Their **.kind()** method returns 'INPUT_OBJECT'. They have a **@.inputFields** array of **GraphQL::InputValue**s, very similar to the fields defined within a normal Object. 404 | 405 | my $obj = GraphQL::InputObjectType.new( 406 | name => 'myinputobject', 407 | inputFields => (GraphQL::InputValue.new(...), GraphQL::InputValue.new(...) 408 | ); 409 | 410 | In *GSL*: 411 | 412 | input myinputobject { 413 | ...inputvalues... 414 | } 415 | 416 | In Perl, you must specify a class explicitly as a GraphQL::InputObject: 417 | 418 | class myinputobject is GraphQL::InputObject { 419 | ...inputvalues... 420 | } 421 | 422 | #### **GraphQL::Union** is **GraphQL::Type** 423 | 424 | A union has **.kind()** = 'UNION', and a **@.possibleTypes** attribute listing the types of the union. 425 | 426 | my $union = GraphQL::Union.new( 427 | name => 'myunion', 428 | possibleTypes => ($someobject, $someotherobject) 429 | ); 430 | 431 | In *GSL*: 432 | 433 | union myunion = someobject | someotherobject 434 | 435 | NOTE: Not yet implemented in Perl classes. 436 | 437 | #### **GraphQL::Enum** is **GraphQL::Type** 438 | 439 | Has **.kind()** = 'ENUM', and **@.enumValues** with a list of **GraphQL::EnumValue**s. The accessor method for **.enumValues()** takes an optional *Bool* argument `:$includeDeprecated` which will either include deprecated values or exclude them. 440 | 441 | my $enum = GraphQL::Enum.new( 442 | name => 'myenum', 443 | enumValues => (GraphQL::EnumValue.new(...), GraphQL::EnumValue.new(...)) 444 | ); 445 | 446 | In *GSL*: 447 | 448 | enum myenum { VAL1 VAL2 ... } 449 | 450 | In Perl: 451 | 452 | enum myenum ; 453 | 454 | #### **GraphQL::Directive** is **GraphQL::Type** 455 | 456 | Still needs work... 457 | -------------------------------------------------------------------------------- /eg/graphiql-schema-request-query: -------------------------------------------------------------------------------- 1 | 2 | query IntrospectionQuery { 3 | __schema { 4 | queryType { name } 5 | mutationType { name } 6 | types { 7 | ...FullType 8 | } 9 | directives { 10 | name 11 | description 12 | locations 13 | args { 14 | ...InputValue 15 | } 16 | } 17 | } 18 | } 19 | 20 | fragment FullType on __Type { 21 | kind 22 | name 23 | description 24 | fields(includeDeprecated: true) { 25 | name 26 | description 27 | args { 28 | ...InputValue 29 | } 30 | type { 31 | ...TypeRef 32 | } 33 | isDeprecated 34 | deprecationReason 35 | } 36 | inputFields { 37 | ...InputValue 38 | } 39 | interfaces { 40 | ...TypeRef 41 | } 42 | enumValues(includeDeprecated: true) { 43 | name 44 | description 45 | isDeprecated 46 | deprecationReason 47 | } 48 | possibleTypes { 49 | ...TypeRef 50 | } 51 | } 52 | 53 | fragment InputValue on __InputValue { 54 | name 55 | description 56 | type { ...TypeRef } 57 | defaultValue 58 | } 59 | 60 | fragment TypeRef on __Type { 61 | kind 62 | name 63 | ofType { 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 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /eg/hello-graphiql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CurtTilmes/Perl6-GraphQL/b05d61c53f52502045609e79d8c01036a65e0ef7/eg/hello-graphiql.png -------------------------------------------------------------------------------- /eg/hello.cro.p6: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl6 2 | 3 | use GraphQL; 4 | use Cro::HTTP::Router::GraphQL; 5 | use Cro::HTTP::Router; 6 | use Cro::HTTP::Server; 7 | 8 | class Query 9 | { 10 | method hello(--> Str) { 'Hello World' } 11 | } 12 | 13 | my $schema = GraphQL::Schema.new(Query); 14 | 15 | my Cro::Service $hello = Cro::HTTP::Server.new: 16 | :host, :port<10000>, 17 | application => route 18 | { 19 | get -> { redirect '/graphql' } 20 | 21 | get -> 'graphql' { graphiql } 22 | 23 | post -> 'graphql' { graphql($schema) } 24 | } 25 | 26 | $hello.start; 27 | 28 | react whenever signal(SIGINT) { $hello.stop; exit; } 29 | -------------------------------------------------------------------------------- /eg/hello.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl6 2 | 3 | use GraphQL; 4 | use GraphQL::Server; 5 | 6 | class Query 7 | { 8 | method hello(--> Str) { 'Hello World' } 9 | } 10 | 11 | my $schema = GraphQL::Schema.new(Query); 12 | 13 | GraphQL-Server($schema); 14 | -------------------------------------------------------------------------------- /eg/users.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl6 2 | 3 | use GraphQL; 4 | use GraphQL::Types; 5 | use GraphQL::Server; 6 | use Cache::LRU; 7 | 8 | my $usercache = Cache::LRU.new; # Key = User.id, Value = User 9 | 10 | #| State is an enumeration of possible states for the user. 11 | enum State ; 12 | 13 | class Query { ... } 14 | class Mutation { ...} 15 | 16 | #| User is for describing users. 17 | class User 18 | { 19 | trusts Mutation; 20 | 21 | #| id field is unique identifier for each user. 22 | has ID $.id; 23 | 24 | #| name field is the name of the user 25 | has Str:D $.name is rw is required; 26 | 27 | #| birthday field is just a string, put whatever you want in it. 28 | has Str $.birthday is rw; 29 | 30 | #| status tells whether the user is true or false. 31 | has Bool $.status is rw; 32 | 33 | #| state demonstrates an enumeration. 34 | has State $.state is rw; 35 | 36 | has Set $!friend-set = ∅; 37 | 38 | #| friends returns an array of all this user's friends. 39 | method friends(--> Array[User]) is graphql-background 40 | { 41 | Array[User].new( 42 | await $!friend-set.keys.map( 43 | { start Query.user(:id($_)) } 44 | ) 45 | ); 46 | } 47 | 48 | #| random_friend picks a single friend from among this user's friends. 49 | method random_friend(--> User) is graphql-background 50 | { 51 | Query.user(:id($!friend-set.pick // return Nil)); 52 | } 53 | 54 | method !friend_add(ID :$friend_id --> Bool) 55 | { 56 | $!friend-set ∪= $friend_id; 57 | $usercache.remove($!id); 58 | return True; 59 | } 60 | 61 | method !friend_remove(ID :$friend_id --> Bool) 62 | { 63 | $!friend-set -= $friend_id; 64 | $usercache.remove($!id); 65 | return True; 66 | } 67 | } 68 | 69 | class UserInput is GraphQL::InputObject 70 | { 71 | has Str $.name; 72 | has Str $.birthday; 73 | has Bool $.status; 74 | has State $.state; 75 | } 76 | 77 | my User @users = 78 | User.new(id => "0", 79 | name => 'Gilligan', 80 | birthday => 'Friday', 81 | status => True, 82 | state => NOT_FOUND), 83 | User.new(id => "1", 84 | name => 'Skipper', 85 | birthday => 'Monday', 86 | status => False, 87 | state => ACTIVE), 88 | User.new(id => "2", 89 | name => 'Professor', 90 | birthday => 'Tuesday', 91 | status => True, 92 | state => INACTIVE), 93 | User.new(id => "3", 94 | name => 'Ginger', 95 | birthday => 'Wednesday', 96 | status => True, 97 | state => SUSPENDED), 98 | User.new(id => "4", 99 | name => 'Mary Anne', 100 | birthday => 'Thursday', 101 | status => True, 102 | state => ACTIVE); 103 | 104 | class Query 105 | { 106 | method user(ID :$id! --> User) 107 | is graphql-background 108 | { 109 | return unless @users[$id]; 110 | 111 | if $usercache.get($id) -> $user 112 | { 113 | return $user; 114 | } 115 | 116 | sleep 2; 117 | 118 | my $user = @users[$id]; 119 | 120 | $usercache.set($id, $user); 121 | } 122 | 123 | method listusers(ID :$start = "0", Int :$count = 3 --> Array[User]) 124 | is graphql-background 125 | { 126 | Array[User].new( 127 | await ($start ..^ $start+$count).map( 128 | { start Query.user(:id($_)) } 129 | ) 130 | ); 131 | } 132 | } 133 | 134 | class Mutation 135 | { 136 | method adduser(UserInput :$newuser! --> ID) 137 | { 138 | push @users, User.new(id => @users.elems, 139 | name => $newuser.name, 140 | birthday => $newuser.birthday, 141 | status => $newuser.status, 142 | state => $newuser.state); 143 | return @users.elems - 1; 144 | } 145 | 146 | method updateuser(ID :$id!, UserInput :$userinput! --> User) 147 | { 148 | for -> $field 149 | { 150 | if $userinput."$field"().defined 151 | { 152 | @users[$id]."$field"() = $userinput."$field"(); 153 | } 154 | } 155 | $usercache.remove($id); 156 | return Query.user(:$id); 157 | } 158 | 159 | method friend_add(ID :$id!, ID :$friend_id! --> Bool) 160 | { 161 | @users[$id]!User::friend_add(:$friend_id); 162 | } 163 | 164 | method friend_remove(ID :$id!, ID :$friend_id! --> Bool) 165 | { 166 | @users[$id]!User::friend_remove(:$friend_id); 167 | } 168 | } 169 | 170 | my $schema = GraphQL::Schema.new(State, User, UserInput, Query, Mutation); 171 | 172 | GraphQL-Server($schema); 173 | -------------------------------------------------------------------------------- /eg/users.schema: -------------------------------------------------------------------------------- 1 | # User is the main type for the server. 2 | type User { 3 | # id is the unique identifier for this User. 4 | id: ID! 5 | # name is the name of this user. 6 | name: String 7 | # birthday doesn't have any real format, it is just another string. 8 | birthday: String 9 | # status doesn't mean anything, just a boolean field you can 10 | # set to true or false 11 | status: Boolean 12 | } 13 | 14 | # UserInput is a special input type you can supply a user input object with. 15 | # For now you get a hash. Maybe we could auto-bless these into real classes? 16 | input UserInput { 17 | name: String 18 | birthday: String 19 | status: Boolean 20 | } 21 | 22 | # Top level queries. 23 | type Query { 24 | listusers(start: ID = "0", count: Int = 1): [User] 25 | user(id: ID!): User 26 | } 27 | 28 | # Top level mutations. 29 | type Mutation { 30 | adduser(newuser: UserInput!): ID 31 | updateuser(id: ID!, userinput: UserInput!): User 32 | } 33 | 34 | # Specify the main top level objects for the schema. 35 | schema { 36 | query: Query 37 | mutation: Mutation 38 | } 39 | -------------------------------------------------------------------------------- /eg/usersexample.md: -------------------------------------------------------------------------------- 1 | # Users example 2 | 3 | The __Schema__ describes the interface for your application in detail. 4 | 5 | You start by describing your __Schema__ in terms of data types. 6 | Starting with the GraphQL core types (String, Int, Float, Boolean, 7 | ID), possibly modified ([List], Non-Null!), and built into a set of 8 | Object Types. You can also define types that are Unions of other 9 | types or Enum enumerations of pre-defined values. 10 | 11 | For example: 12 | ``` 13 | type User { 14 | id: ID! 15 | name: String 16 | birthday: String 17 | status: Boolean 18 | } 19 | ``` 20 | 21 | I've used 'String' as the type for birthday. There isn't a core 22 | GraphQL 'Date' or 'DateTime' type -- though other languages frequently 23 | implement it. We'll probably add that to the Perl6 version soon. For 24 | this example, it is just a string. 25 | 26 | ### Descriptions 27 | 28 | Though not (yet) part of the standard, another frequently implemented 29 | extension is the ability to add descriptions to types and fields with 30 | \# comments. If you look at the 31 | [eg/users.schema](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/users.schema) 32 | file, you'll see # descriptions for some of the types and fields. 33 | Those descriptions can be queried with the GraphQL introspection 34 | queries through the meta types and queries __Schema, __Type, etc. The 35 | GraphiQL IDE displays them while you explore the schema with the Docs 36 | functionality. 37 | 38 | ## Types with arguments 39 | 40 | Object fields can also include arguments. To query our User database, 41 | we'll define two such queries: 42 | 43 | ``` 44 | type Query { 45 | listusers(start: ID = "0", count: Int = "1"): [User] 46 | user(id: ID!): User 47 | } 48 | ``` 49 | 50 | So you can list *count* users starting with a specific *id*, or just 51 | query a single user. 52 | 53 | ## Resolvers 54 | 55 | Now that we've defined the external GraphQL API, we need to define 56 | functions that resolve those queries. 57 | 58 | First a Perl class to act like our GraphQL *User* type: 59 | 60 | ``` 61 | class User 62 | { 63 | has Int $.id is rw; 64 | has Str $.name is rw; 65 | has Str $.birthday is rw; 66 | has Bool $.status is rw; 67 | } 68 | ``` 69 | 70 | and a pseudo-database to hold our users: 71 | ``` 72 | my @users = User.new(id => 0, name => '...', birthday => '...', status => True), 73 | User.new(...), 74 | ...; 75 | ``` 76 | 77 | Then a few simple functions to implement listusers() and user(), 78 | matching the argument list defined in the GraphQL schema: 79 | 80 | ``` 81 | my $resolvers = 82 | { 83 | Query => 84 | { 85 | listusers => sub (:$start, Int :$count) 86 | { 87 | @users[$start ..^ $start+$count] 88 | }, 89 | 90 | user => sub (:$id) 91 | { 92 | @users[$id] 93 | } 94 | } 95 | }; 96 | ``` 97 | 98 | *user()* just returns the user specified by $id, and *listusers()* 99 | returns *count* of them starting with id *start* and returns them in 100 | an *Array* which gets mapped to the GraphQL *List*. 101 | 102 | Then create your GraphQL::Schema : 103 | 104 | ``` 105 | my $schema = GraphQL::Schema.new(...schema here..., resolvers => $resolvers); 106 | ``` 107 | 108 | If you put your schema in a separate file, you can plug it in easily 109 | with IO.slurp (see the 110 | [example](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/usersserver.pl)). 111 | 112 | 113 | Running this server under 114 | [Bailador](https://github.com/ufobat/Bailador), you can explore the 115 | schema interactively, and execute our queries. For example: 116 | 117 | ``` 118 | { 119 | user(id: 0) { 120 | name 121 | birthday 122 | } 123 | } 124 | ``` 125 | 126 | to see the name and birthday of user 0. 127 | 128 | Note that in the **hello** example, the resolver was specified down to 129 | the *Field* level returning a *Scalar* (*String*), while here, the 130 | resolvers return an *Object* or a *List* of *Object*s. If you return 131 | a Perl 6 Class, it must include methods for resolving each field of 132 | the type. (e.g. here, we have methods for name(), birthday(), 133 | etc. defined by Perl because they are public). You can actually mix 134 | and match if you like, defining individual resolvers for some fields, 135 | while relying on Class methods for others. If you want to define 136 | both, you'll have to call $schema.resolvers() multiple times, once 137 | with the resolver for the *Object* level, and once for the fields of 138 | that object. 139 | 140 | ## Mutations 141 | 142 | Querying is nice, but what if you want to allow changes? (Hopefully 143 | only by trusted, authenticated, authorized users.) 144 | 145 | In reality, there isn't really anything special about Mutations, and 146 | if you wanted to do an update with a normal query, nothing technical 147 | would stop you. It is highly recommended, however, that you group such 148 | queries and explicitly declare them as mutations. This has a few real 149 | consequences. For one, the server will always execute mutations 150 | serially though it is allowed to execute normal queries in parallel. 151 | This will prevent race conditions and race induced non-deterministic 152 | behaviors. For another, client tools can assume that normal queries 153 | can be cached, while mutations never will be. 154 | 155 | We can define a special kind of object called an *InputObject*, that 156 | looks almost like a normal type: 157 | 158 | ``` 159 | input UserInput { 160 | name: String 161 | birthday: String 162 | status: Boolean 163 | } 164 | ``` 165 | 166 | and define some mutations in the schema: 167 | 168 | ``` 169 | type Mutation { 170 | adduser(newuser: UserInput!): ID 171 | updateuser(id: ID!, userinput: UserInput!): User 172 | } 173 | ``` 174 | 175 | and implement some matching resolvers for those: 176 | 177 | ``` 178 | adduser => sub (:%newuser) 179 | { 180 | push @users, User.new(id => @users.elems, |%newuser); 181 | return @users.elems - 1; 182 | }, 183 | 184 | updateuser => sub (:$id, :%userinput) 185 | { 186 | for %userinput.kv -> $k, $v 187 | { 188 | @users[$id]."$k"() = $v; 189 | } 190 | 191 | return @users[$id] 192 | } 193 | ``` 194 | 195 | See the full schema in 196 | [users.schema](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/users.schema), 197 | and the example Bailador server in 198 | [usersserver.pl](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/usersserver.pl). 199 | 200 | ## Some sample queries 201 | 202 | List the first three users, with their names: 203 | ``` 204 | { 205 | listusers(count:3) { 206 | name 207 | } 208 | } 209 | ``` 210 | 211 | Get the birthday and status for user 2: 212 | ``` 213 | { 214 | user(id: 2){ 215 | birthday 216 | status 217 | } 218 | } 219 | ``` 220 | 221 | Add a new user named "John" (null status and birthday because they 222 | aren't specified) 223 | ``` 224 | mutation { 225 | adduser(newuser: {name: "John"}) 226 | } 227 | ``` 228 | 229 | Set John's birthday to "Every Year", and return his name and birthday: 230 | ``` 231 | mutation { 232 | updateuser(id: "5", userinput: { birthday: "Every Year" }) { 233 | name 234 | birthday 235 | } 236 | } 237 | ``` 238 | 239 | ## Notes 240 | 241 | Again, this isn't really a production quality server. You should do 242 | more with authentication/authorization, sessions, setting 243 | content-types, etc. This server also ignores variables supplied by 244 | the user. Those should also be passed in to the schema execute() 245 | call. In the future, some more of that work may be added in to this 246 | repository. 247 | 248 | Also parsing in Perl 6 is still very slow, so a simple optimization is 249 | to cache parsed documents, and just re-use the parsed document if the 250 | same query is sent. (When you build GraphQL into User interfaces, the 251 | same query documents are frequently reused.) 252 | 253 | You can see how that would work:. 254 | 255 | Instead of: 256 | ``` 257 | $schema.execute('some query'); 258 | ``` 259 | 260 | use the .document() method to parse it, and the :document named 261 | parameter to execute to specify an already parsed document (a 262 | *GraphQL::Document*). 263 | 264 | ``` 265 | my $document = $schema.document('some query'); 266 | $schema.execute(:$document); 267 | ``` 268 | -------------------------------------------------------------------------------- /eg/usersschema.md: -------------------------------------------------------------------------------- 1 | # Users example 2 | 3 | ## Defining a schema 4 | 5 | The __Schema__ describes the interface for your application in detail. 6 | 7 | The GraphQL Schema language is described in detail at 8 | http://graphql.org. There is also a cheat sheet that can be useful 9 | ([pdf](https://github.com/sogko/graphql-shorthand-notation-cheat-sheet/raw/master/graphql-shorthand-notation-cheat-sheet.pdf) 10 | [png](https://raw.githubusercontent.com/sogko/graphql-shorthand-notation-cheat-sheet/master/graphql-shorthand-notation-cheat-sheet.png)). 11 | 12 | 13 | You start by describing your __Schema__ in terms of data types. 14 | Starting with the GraphQL core types (String, Int, Float, Boolean, 15 | ID), possibly modified ([List], Non-Null!), and built into a set of 16 | Object Types. You can also define types that are Unions of other 17 | types or Enum enumerations of pre-defined values. 18 | 19 | For example: 20 | ``` 21 | type User { 22 | id: ID! 23 | name: String 24 | birthday: String 25 | status: Boolean 26 | } 27 | ``` 28 | 29 | I've used 'String' as the type for birthday. There isn't a core 30 | GraphQL 'Date' or 'DateTime' type -- though other languages frequently 31 | implement it. We'll probably add that to the Perl6 version soon. For 32 | this example, it is just a string. 33 | 34 | ### Descriptions 35 | 36 | Though not (yet) part of the standard, another frequently implemented 37 | extension is the ability to add descriptions to types and fields with 38 | \# comments. If you look at the 39 | [eg/users.schema](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/users.schema) 40 | file, you'll see # descriptions for some of the types and fields. 41 | Those descriptions can be queried with the GraphQL introspection 42 | queries through the meta types and queries __Schema, __Type, etc. The 43 | GraphiQL IDE displays them while you explore the schema with the Docs 44 | functionality. 45 | 46 | ## Types with arguments 47 | 48 | Object fields can also include arguments. To query our User database, 49 | we'll define two such queries: 50 | 51 | ``` 52 | type Query { 53 | listusers(start: ID = "0", count: Int = "1"): [User] 54 | user(id: ID!): User 55 | } 56 | ``` 57 | 58 | So you can list *count* users starting with a specific *id*, or just 59 | query a single user. 60 | 61 | ## Resolvers 62 | 63 | Now that we've defined the external GraphQL API, we need to define 64 | functions that resolve those queries. 65 | 66 | First a Perl class to act like our GraphQL *User* type: 67 | 68 | ``` 69 | class User 70 | { 71 | has Int $.id is rw; 72 | has Str $.name is rw; 73 | has Str $.birthday is rw; 74 | has Bool $.status is rw; 75 | } 76 | ``` 77 | 78 | and a pseudo-database to hold our users: 79 | ``` 80 | my @users = User.new(id => 0, name => '...', birthday => '...', status => True), 81 | User.new(...), 82 | ...; 83 | ``` 84 | 85 | Then a few simple functions to implement listusers() and user(), 86 | matching the argument list defined in the GraphQL schema: 87 | 88 | ``` 89 | my $resolvers = 90 | { 91 | Query => 92 | { 93 | listusers => sub (:$start, Int :$count) 94 | { 95 | @users[$start ..^ $start+$count] 96 | }, 97 | 98 | user => sub (:$id) 99 | { 100 | @users[$id] 101 | } 102 | } 103 | }; 104 | ``` 105 | 106 | *user()* just returns the user specified by $id, and *listusers()* 107 | returns *count* of them starting with id *start* and returns them in 108 | an *Array* which gets mapped to the GraphQL *List*. 109 | 110 | Then create your GraphQL::Schema : 111 | 112 | ``` 113 | my $schema = GraphQL::Schema.new(...schema here..., resolvers => $resolvers); 114 | ``` 115 | 116 | If you put your schema in a separate file, you can plug it in easily 117 | with IO.slurp (see the 118 | [example](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/usersserver.pl)). 119 | 120 | 121 | Running this server under 122 | [Bailador](https://github.com/ufobat/Bailador), you can explore the 123 | schema interactively, and execute our queries. For example: 124 | 125 | ``` 126 | { 127 | user(id: 0) { 128 | name 129 | birthday 130 | } 131 | } 132 | ``` 133 | 134 | to see the name and birthday of user 0. 135 | 136 | Note that in the **hello** example, the resolver was specified down to 137 | the *Field* level returning a *Scalar* (*String*), while here, the 138 | resolvers return an *Object* or a *List* of *Object*s. If you return 139 | a Perl 6 Class, it must include methods for resolving each field of 140 | the type. (e.g. here, we have methods for name(), birthday(), 141 | etc. defined by Perl because they are public). You can actually mix 142 | and match if you like, defining individual resolvers for some fields, 143 | while relying on Class methods for others. If you want to define 144 | both, you'll have to call $schema.resolvers() multiple times, once 145 | with the resolver for the *Object* level, and once for the fields of 146 | that object. 147 | 148 | ## Mutations 149 | 150 | Querying is nice, but what if you want to allow changes? (Hopefully 151 | only by trusted, authenticated, authorized users.) 152 | 153 | In reality, there isn't really anything special about Mutations, and 154 | if you wanted to do an update with a normal query, nothing technical 155 | would stop you. It is highly recommended, however, that you group such 156 | queries and explicitly declare them as mutations. This has a few real 157 | consequences. For one, the server will always execute mutations 158 | serially though it is allowed to execute normal queries in parallel. 159 | This will prevent race conditions and race induced non-deterministic 160 | behaviors. For another, client tools can assume that normal queries 161 | can be cached, while mutations never will be. 162 | 163 | We can define a special kind of object called an *InputObject*, that 164 | looks almost like a normal type: 165 | 166 | ``` 167 | input UserInput { 168 | name: String 169 | birthday: String 170 | status: Boolean 171 | } 172 | ``` 173 | 174 | and define some mutations in the schema: 175 | 176 | ``` 177 | type Mutation { 178 | adduser(newuser: UserInput!): ID 179 | updateuser(id: ID!, userinput: UserInput!): User 180 | } 181 | ``` 182 | 183 | and implement some matching resolvers for those: 184 | 185 | ``` 186 | adduser => sub (:%newuser) 187 | { 188 | push @users, User.new(id => @users.elems, |%newuser); 189 | return @users.elems - 1; 190 | }, 191 | 192 | updateuser => sub (:$id, :%userinput) 193 | { 194 | for %userinput.kv -> $k, $v 195 | { 196 | @users[$id]."$k"() = $v; 197 | } 198 | 199 | return @users[$id] 200 | } 201 | ``` 202 | 203 | See the full schema in 204 | [users.schema](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/users.schema), 205 | and the example Bailador server in 206 | [usersserver.pl](https://github.com/golpa/Perl6-GraphQL/blob/master/eg/usersserver.pl). 207 | 208 | ## Some sample queries 209 | 210 | List the first three users, with their names: 211 | ``` 212 | { 213 | listusers(count:3) { 214 | name 215 | } 216 | } 217 | ``` 218 | 219 | Get the birthday and status for user 2: 220 | ``` 221 | { 222 | user(id: 2){ 223 | birthday 224 | status 225 | } 226 | } 227 | ``` 228 | 229 | Add a new user named "John" (null status and birthday because they 230 | aren't specified) 231 | ``` 232 | mutation { 233 | adduser(newuser: {name: "John"}) 234 | } 235 | ``` 236 | 237 | Set John's birthday to "Every Year", and return his name and birthday: 238 | ``` 239 | mutation { 240 | updateuser(id: "5", userinput: { birthday: "Every Year" }) { 241 | name 242 | birthday 243 | } 244 | } 245 | ``` 246 | 247 | ## Notes 248 | 249 | Again, this isn't really a production quality server. You should do 250 | more with authentication/authorization, sessions, setting 251 | content-types, etc. This server also ignores variables supplied by 252 | the user. Those should also be passed in to the schema execute() 253 | call. In the future, some more of that work may be added in to this 254 | repository. 255 | 256 | Also parsing in Perl 6 is still very slow, so a simple optimization is 257 | to cache parsed documents, and just re-use the parsed document if the 258 | same query is sent. (When you build GraphQL into User interfaces, the 259 | same query documents are frequently reused.) 260 | 261 | You can see how that would work:. 262 | 263 | Instead of: 264 | ``` 265 | $schema.execute('some query'); 266 | ``` 267 | 268 | use the .document() method to parse it, and the :document named 269 | parameter to execute to specify an already parsed document (a 270 | *GraphQL::Document*). 271 | 272 | ``` 273 | my $document = $schema.document('some query'); 274 | $schema.execute(:$document); 275 | ``` 276 | -------------------------------------------------------------------------------- /eg/usersschema.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl6 2 | 3 | use GraphQL; 4 | use GraphQL::Server; 5 | 6 | class User 7 | { 8 | has Int $.id is rw; 9 | has Str $.name is rw; 10 | has Str $.birthday is rw; 11 | has Bool $.status is rw; 12 | } 13 | 14 | my @users = 15 | User.new(id => 0, 16 | name => 'Gilligan', 17 | birthday => 'Friday', 18 | status => True), 19 | User.new(id => 1, 20 | name => 'Skipper', 21 | birthday => 'Monday', 22 | status => False), 23 | User.new(id => 2, 24 | name => 'Professor', 25 | birthday => 'Tuesday', 26 | status => True), 27 | User.new(id => 3, 28 | name => 'Ginger', 29 | birthday => 'Wednesday', 30 | status => True), 31 | User.new(id => 4, 32 | name => 'Mary Anne', 33 | birthday => 'Thursday', 34 | status => True); 35 | 36 | my $resolvers = 37 | { 38 | Query => 39 | { 40 | listusers => sub (:$start, Int :$count) 41 | { 42 | @users[$start ..^ $start+$count] 43 | }, 44 | 45 | user => sub (:$id) 46 | { 47 | @users[$id] 48 | } 49 | }, 50 | Mutation => 51 | { 52 | adduser => sub (:%newuser) 53 | { 54 | push @users, User.new(id => @users.elems, |%newuser); 55 | return @users.elems - 1; 56 | }, 57 | 58 | updateuser => sub (:$id, :%userinput) 59 | { 60 | for %userinput.kv -> $k, $v 61 | { 62 | @users[$id]."$k"() = $v; 63 | } 64 | 65 | return @users[$id] 66 | } 67 | } 68 | }; 69 | 70 | my $schema = GraphQL::Schema.new("users.schema".IO.slurp, 71 | resolvers => $resolvers); 72 | 73 | GraphQL-Server($schema); 74 | -------------------------------------------------------------------------------- /lib/GraphQL.pm: -------------------------------------------------------------------------------- 1 | unit module GraphQL; 2 | 3 | use GraphQL::Introspection; 4 | use GraphQL::Grammar; 5 | use GraphQL::Actions; 6 | use GraphQL::Types; 7 | use GraphQL::Response; 8 | use GraphQL::Execution; 9 | use GraphQL::Validation; 10 | 11 | multi sub trait_mod:(Method $m, :$graphql-background!) is export { ... } 12 | 13 | my Set $defaultTypes = set GraphQLInt, GraphQLFloat, GraphQLString, 14 | GraphQLBoolean, GraphQLID; 15 | 16 | class GraphQL::Schema 17 | { 18 | has GraphQL::Type %!types; 19 | has GraphQL::Directive @.directives; 20 | has Str $.query is rw = 'Query'; 21 | has Str $.mutation is rw; 22 | has Str $.subscription is rw; 23 | has %.stash is rw; 24 | has @.errors; 25 | has $!resolved-schema; 26 | 27 | multi method new(:$query, :$mutation, :$subscription, :$resolvers, *@types, 28 | *%stash-vars) 29 | returns GraphQL::Schema 30 | { 31 | my $schema = GraphQL::Schema.bless; 32 | $schema.stash{%stash-vars.keys} = %stash-vars.values; 33 | 34 | $schema.add-type($defaultTypes.keys); 35 | 36 | my $actions = GraphQL::Actions.new(:$schema); 37 | 38 | GraphQL::Grammar.parse($GraphQL-Introspection-Schema, 39 | :$actions, 40 | rule => 'TypeSchema') 41 | or die "Failed to parse Introspection Schema"; 42 | 43 | $schema.add-type(@types); 44 | 45 | $schema.query = $query if $query; 46 | $schema.mutation = $mutation if $mutation; 47 | $schema.subscription = $subscription if $subscription; 48 | 49 | $schema.resolvers($resolvers) if $resolvers; 50 | 51 | return $schema; 52 | } 53 | 54 | multi method new(Str $schemastring, :$resolvers, *%stash-vars) 55 | returns GraphQL::Schema 56 | { 57 | my $schema = GraphQL::Schema.new; 58 | $schema.stash{%stash-vars.keys} = %stash-vars.values; 59 | 60 | my $actions = GraphQL::Actions.new(:$schema); 61 | 62 | GraphQL::Grammar.parse($schemastring, :$actions, :rule('TypeSchema')) 63 | or die "Failed to parse schema"; 64 | 65 | $schema.resolvers($resolvers) if $resolvers; 66 | 67 | return $schema; 68 | } 69 | 70 | method !add-meta-fields 71 | { 72 | self.queryType.addfield(GraphQL::Field.new( 73 | name => '__type', 74 | type => self.type('__Type'), 75 | args => [ GraphQL::InputValue.new( 76 | name => 'name', 77 | type => GraphQL::Non-Null.new( 78 | ofType => GraphQLString 79 | ) 80 | ) 81 | ], 82 | resolver => sub (:$name) { self.type($name) } 83 | )); 84 | 85 | self.queryType.addfield(GraphQL::Field.new( 86 | name => '__schema', 87 | type => GraphQL::Non-Null.new( 88 | ofType => self.type('__Schema') 89 | ), 90 | resolver => sub { self } 91 | )); 92 | } 93 | 94 | has %!resolved; 95 | 96 | method resolve-type(GraphQL::Type $type) 97 | { 98 | return $type if $type ~~ GraphQL::Object and %!resolved{$type.name}++; 99 | 100 | given $type 101 | { 102 | when GraphQL::LazyType 103 | { 104 | my $realtype = self.type($type.name); 105 | 106 | die "Can't resolve $type.name()" 107 | if $realtype ~~ GraphQL::LazyType; 108 | 109 | return $realtype; 110 | } 111 | when GraphQL::Interface 112 | { 113 | $type.fieldlist .= map({ self.resolve-type($_) }); 114 | } 115 | when GraphQL::Object 116 | { 117 | $type.fieldlist .= map({ self.resolve-type($_) }); 118 | 119 | $type.interfaces .= map({ self.resolve-type($_) }); 120 | 121 | push $type.fieldlist, GraphQL::Field.new( 122 | name => '__typename', 123 | type => GraphQLString, 124 | resolver => sub { $type.name }); 125 | 126 | for $type.interfaces -> $int 127 | { 128 | unless $int.possibleTypes.first($type) 129 | { 130 | push $int.possibleTypes, $type; 131 | } 132 | } 133 | } 134 | when GraphQL::Input 135 | { 136 | $type.inputFields .= map({ self.resolve-type($_) }); 137 | } 138 | when GraphQL::Union 139 | { 140 | $type.possibleTypes .= map({ self.resolve-type($_) });; 141 | } 142 | when GraphQL::Non-Null | GraphQL::List 143 | { 144 | $type.ofType = self.resolve-type($type.ofType); 145 | } 146 | when GraphQL::Field 147 | { 148 | $type.type = self.resolve-type($type.type); 149 | $type.args .= map({ self.resolve-type($_) }); 150 | } 151 | when GraphQL::InputValue 152 | { 153 | $type.type = self.resolve-type($type.type); 154 | } 155 | } 156 | return $type; 157 | } 158 | 159 | method resolve-schema 160 | { 161 | die "Must define root query type" unless self.queryType() 162 | and self.queryType ~~ GraphQL::Object; 163 | 164 | if not $!mutation.defined and self.type('Mutation') 165 | and self.type('Mutation') ~~ GraphQL::Object 166 | { 167 | $!mutation = 'Mutation'; 168 | } 169 | 170 | self!add-meta-fields; 171 | 172 | for %!types.values -> $type 173 | { 174 | self.resolve-type($type); 175 | } 176 | } 177 | 178 | method types { %!types.values } 179 | 180 | method add-type(*@newtypes) 181 | { 182 | for @newtypes 183 | { 184 | when GraphQL::Type { %!types{.name} = $_; } 185 | 186 | when GraphQL::InputObject { self.add-inputobject($_) } 187 | 188 | when Enumeration { self.add-enum($_) } 189 | 190 | default { self.add-class($_) } 191 | } 192 | } 193 | 194 | method maketype(Str $rule, Str $desc) returns GraphQL::Type 195 | { 196 | my $actions = GraphQL::Actions.new(:schema(self)); 197 | 198 | GraphQL::Grammar.parse($desc, :$rule, :$actions).made; 199 | } 200 | 201 | method add-class($t) 202 | { 203 | my @fields; 204 | 205 | for $t.^attributes -> $a 206 | { 207 | next unless $a.has_accessor; 208 | 209 | my $var = $a ~~ /<-[!]>+$/; 210 | 211 | my $name = $var.Str; 212 | 213 | next unless $name ~~ /^<[_A..Za..z]><[_0..9A..Za..z]>*$/; 214 | 215 | next unless $a.type ~~ Any; 216 | 217 | my $type = self.perl-type($a.type); 218 | 219 | my $description = $a.WHY ?? ~$a.WHY !! Str; 220 | 221 | push @fields, GraphQL::Field.new(:$name, :$type, :$description); 222 | } 223 | 224 | for $t.^methods -> $m 225 | { 226 | next if @fields.first: { $m.name eq .name }; 227 | 228 | next if $m.name eq 'new'|'BUILD'; 229 | 230 | next unless $m.name ~~ /^<[_A..Za..z]><[_0..9A..Za..z]>*$/; 231 | 232 | my GraphQL::InputValue @args; 233 | 234 | my $sig = $m.signature; 235 | 236 | next unless $sig.returns ~~ Any; 237 | 238 | my $type = self.perl-type($sig.returns); 239 | 240 | for $sig.params -> $p 241 | { 242 | next unless $p.named; 243 | 244 | my $name = $p.named_names[0] or next; 245 | 246 | next unless $name ~~ /^<[_A..Za..z]><[_0..9A..Za..z]>*$/; 247 | 248 | next unless $p.type ~~ Any; 249 | 250 | my $type = self.perl-type($p.type); 251 | 252 | $type = GraphQL::Non-Null.new(ofType => $type) 253 | unless $p.optional; 254 | 255 | my $defaultValue = $p.default ?? $p.default.() !! Nil; 256 | 257 | push @args, GraphQL::InputValue.new(:$name, 258 | :$type, 259 | :$defaultValue); 260 | } 261 | 262 | my $description = $m.WHY ?? ~$m.WHY !! Str; 263 | 264 | push @fields, GraphQL::Field.new(:name($m.name), 265 | :$type, 266 | :@args, 267 | :resolver($m), 268 | :$description); 269 | } 270 | 271 | my $description = $t.WHY ?? ~$t.WHY !! Str; 272 | 273 | self.add-type(GraphQL::Object.new(name => $t.^name, 274 | fieldlist => @fields, 275 | :$description)); 276 | } 277 | 278 | method add-inputobject($t) 279 | { 280 | my @inputfields; 281 | 282 | for $t.^attributes -> $a 283 | { 284 | my $var = $a ~~ /<-[!]>+$/; 285 | my $name = $var.Str; 286 | 287 | my $type = self.perl-type($a.type); 288 | 289 | my $description = $a.WHY ?? ~$a.WHY !! Str; 290 | 291 | push @inputfields, GraphQL::InputValue.new(:$name, 292 | :$type, 293 | :$description); 294 | } 295 | 296 | my $description = $t.WHY ?? ~$t.WHY !! Str; 297 | 298 | self.add-type(GraphQL::Input.new(name => $t.^name, 299 | inputFields => @inputfields, 300 | class => $t, 301 | :$description)); 302 | } 303 | 304 | method add-enum(Enumeration $t) 305 | { 306 | my $description = $t.WHY ?? ~$t.WHY !! Str; 307 | 308 | self.add-type(GraphQL::Enum.new( 309 | name => $t.^name, 310 | enum => $t, 311 | :$description, 312 | enumValues => $t.enums.map({ 313 | GraphQL::EnumValue.new(name => .key) 314 | }) 315 | )); 316 | } 317 | 318 | method type(Str $name) returns GraphQL::Type 319 | { 320 | %!types{$name} // GraphQL::LazyType.new(:$name); 321 | } 322 | 323 | method perl-type($type, Bool :$nonnull) returns GraphQL::Type 324 | { 325 | # There must be a better way... 326 | if not $nonnull and $type.WHAT.perl ~~ /\:D$/ 327 | { 328 | return GraphQL::Non-Null.new( 329 | ofType => self.perl-type($type, :nonnull) 330 | ); 331 | } 332 | 333 | do given $type 334 | { 335 | when Enumeration { self.type(.^name) } 336 | 337 | when Positional 338 | { 339 | GraphQL::List.new(ofType => self.perl-type($type.of)); 340 | } 341 | 342 | when Bool { GraphQLBoolean } 343 | when Str { GraphQLString } 344 | when Int { GraphQLInt } 345 | when Num { GraphQLFloat } 346 | when Cool { GraphQLID } 347 | 348 | default 349 | { 350 | self.type(.^name); 351 | } 352 | } 353 | } 354 | 355 | method queryType returns GraphQL::Object 356 | { 357 | return ($!query and %!types{$!query}:exists and 358 | %!types{$!query} ~~ GraphQL::Object) 359 | ?? %!types{$!query} 360 | !! Nil; 361 | } 362 | 363 | method mutationType returns GraphQL::Object 364 | { 365 | return ($!mutation and %!types{$!mutation}) 366 | ?? %!types{$!mutation} 367 | !! Nil; 368 | } 369 | 370 | method subscriptionType returns GraphQL::Object 371 | { 372 | return unless $!subscription and %!types{$!subscription}; 373 | %!types{$!subscription} 374 | } 375 | 376 | method directives { [] } 377 | 378 | method Str 379 | { 380 | self.resolve-schema; 381 | 382 | my $str = ''; 383 | 384 | for %!types.kv -> $typename, $type 385 | { 386 | next if $type ∈ $defaultTypes or $typename ~~ /^__/; 387 | $str ~= $type.Str ~ "\n"; 388 | } 389 | 390 | $str ~= "schema \{\n"; 391 | $str ~= " query: $!query\n"; 392 | $str ~= " mutation: $!mutation\n" if $!mutation; 393 | $str ~= "}\n"; 394 | } 395 | 396 | method document(Str $query) returns GraphQL::Document 397 | { 398 | my $actions = GraphQL::Actions.new(:schema(self)); 399 | 400 | GraphQL::Grammar.parse($query, :$actions, 401 | rule => 'Document') 402 | or die "Failed to parse query"; 403 | 404 | my $document = $/.made; 405 | 406 | self.resolve-schema unless $!resolved-schema++; 407 | 408 | ValidateDocument(:$document, schema => self) 409 | or die "Document validation failed."; 410 | 411 | return $document; 412 | } 413 | 414 | method resolvers(%resolvers) 415 | { 416 | for %resolvers.kv -> $type, $obj 417 | { 418 | die "Undefined object $type" unless %!types{$type}; 419 | 420 | for $obj.kv -> $field, $resolver 421 | { 422 | die "Undefined field $field for $type" 423 | unless %!types{$type}.field($field); 424 | 425 | %!types{$type}.field($field).resolver = $resolver; 426 | } 427 | } 428 | } 429 | 430 | method error(:$message) 431 | { 432 | push @!errors, GraphQL::Error.new(:$message); 433 | } 434 | 435 | method execute(Str $query?, 436 | GraphQL::Document :document($doc), 437 | Str :$operationName, 438 | :%variables, 439 | :$initialValue, 440 | *%session) 441 | { 442 | self.resolve-schema unless $!resolved-schema++; 443 | 444 | %session{%!stash.keys} = %!stash.values; 445 | 446 | @!errors = (); 447 | 448 | my $ret; 449 | 450 | try 451 | { 452 | my $document = $doc // self.document($query); 453 | 454 | $ret = ExecuteRequest(:$document, 455 | :$operationName, 456 | :%variables, 457 | :$initialValue, 458 | schema => self, 459 | :%session); 460 | 461 | CATCH { 462 | default { 463 | self.error(message => .Str); 464 | } 465 | } 466 | } 467 | 468 | my @response; 469 | 470 | if $ret 471 | { 472 | push @response, GraphQL::Response.new( 473 | name => 'data', 474 | type => GraphQL::Object, 475 | value => $ret 476 | ); 477 | } 478 | 479 | if @!errors 480 | { 481 | push @response, GraphQL::Response.new( 482 | name => 'errors', 483 | type => GraphQL::List.new(ofType => GraphQL::Object), 484 | value => @!errors 485 | ); 486 | } 487 | 488 | return GraphQL::Response.new( 489 | type => GraphQL::Object, 490 | value => @response 491 | ); 492 | } 493 | } 494 | 495 | =begin pod 496 | 497 | =head1 GraphQL 498 | 499 | =head2 SYNOPSIS 500 | 501 | use GraphQL; 502 | 503 | class Query 504 | { 505 | method hello(--> Str) { 'Hello World' } 506 | } 507 | 508 | my $schema = GraphQL::Schema.new(Query); 509 | 510 | say $schema.execute('{ hello }').to-json; 511 | 512 | =head2 DESCRIPTION 513 | 514 | "GraphQL is a query language for APIs and a runtime for fulfilling 515 | those queries with your existing data. GraphQL provides a complete and 516 | understandable description of the data in your API, gives clients the 517 | power to ask for exactly what they need and nothing more, makes it 518 | easier to evolve APIs over time, and enables powerful developer 519 | tools." - Facebook Inc., L>. 520 | 521 | The GraphQL Language is described in detail at 522 | L> which also includes the draft specification. 523 | This module is a Perl 6 server implementation of that specification 524 | (or will be once it is complete). The intent of this documentation 525 | isn't to fully describe GraphQL and its usage, but rather to describe 526 | that Perl implementation and how various functionality is accessible 527 | through Perl. This document will assume basic awareness of GraphQL 528 | and that standard. 529 | 530 | =head2 OVERVIEW 531 | 532 | GraphQL itself isn't a database, it is the interface between the 533 | client and whatever database or other data store you use. 534 | Constructing a GraphQL server consists of describing your API 535 | B consisting of a data structure of data B, and 536 | connecting to subroutines or methods for B of the actual 537 | data values. The B is the controller or orchestrator for 538 | everything. It performs two major functions, B to 539 | determine if a query is valid at all, and B, which makes 540 | calls to arbitrary code for B to determine the resulting 541 | data structure. The GraphQL language also specifies B 542 | which is essentially B carried out by the Schema itself to 543 | describe itself. 544 | 545 | The synopsis above describes the simplest GraphQL server possible. It 546 | consists of a single Type or Class called B, with a single 547 | field in it called I of type String, with a method attached to 548 | it that returns the string C. 549 | 550 | The schema is constructed by passing the Perl 6 class into the 551 | C's C constructor. The example then passes in 552 | the simplest GraphQL query K<{hello}>. Execution will call the 553 | C method and return the result in a C 554 | structure that can then be converted into JSON with C 555 | method which will return the result: 556 | 557 | { 558 | "data": { 559 | "hello": "Hello World" 560 | } 561 | } 562 | 563 | In a typical GraphQL Web server, the query would be HTTP POSTed to an 564 | endpoint at C which would call C 565 | and send the resulting JSON string back to the requester. 566 | 567 | Each of those steps will be described in more detail below. 568 | 569 | =head2 Schema Styles 570 | 571 | This module currently supports three different I for 572 | expressing GraphQL types for your GraphQL schema: 573 | 574 | =item B - You can construct each type by creating and nesting 575 | various C objects. 576 | 577 | For the "Hello World" example, it would look like this: 578 | 579 | =begin code 580 | my $schema = GraphQL::Schema.new( 581 | GraphQL::Object.new( 582 | name => 'Query', 583 | fieldlist => GraphQL::Field.new( 584 | name => 'hello', 585 | type => GraphQLString, 586 | resolver => sub { 'Hello World' } 587 | ) 588 | ) 589 | ); 590 | =end code 591 | 592 | =item B or B- The Perl 6 GraphQL engine 593 | includes a complete parser for the I 594 | described in detail at L>. It is important to 595 | note that this is a U language from the I which will be described later. There is also a handy cheat 597 | sheet for the I at 598 | L. 599 | 600 | For the "Hello World" example, it would look like this: 601 | 602 | =begin code 603 | my $schema = GraphQL::Schema.new('type Query { hello: String }', 604 | resolvers => { Query => { hello => sub { 'Hello World' } } }); 605 | =end code 606 | 607 | Note that while the schema type descriptions are provided in the 608 | I, the resolving functions for each field must be separately 609 | supplied in a two level hash with the names of each Object Type at the 610 | first level, and Field at the second level. 611 | 612 | =item B - You can also simply pass in Perl 6 613 | classes directly. A matching schema is constructed by examining the 614 | classes with the Perl language Metamodel for introspection. Given the 615 | GraphQL type restrictions, not everything you can express in Perl will 616 | result in a valid Schema, so it is important to use only the types as 617 | described below. Also restrict the names of attributes and methods to 618 | the alpha-numeric and '_'. (No fancy unicode names, or kebab-case 619 | names.) 620 | 621 | For the "Hello World" example, it looks like this: 622 | 623 | =begin code 624 | class Query 625 | { 626 | method hello(--> Str) { 'Hello World' } 627 | } 628 | 629 | my $schema = GraphQL::Schema.new(Query); 630 | =end code 631 | 632 | Under the hood, the Schemas all look the same, regardless of which 633 | style you use to construct them. The later two options are just 634 | additional syntactic sugar to make things easier. You can also mix 635 | and match, making some types one way and some another and everything 636 | will still work fine. 637 | 638 | =head2 Types 639 | 640 | GraphQL is a strongly, staticly typed language. Every type must be 641 | defined precisely up front, and all can be checked during validation 642 | phase prior to execution. 643 | 644 | The Perl Class hierarchy for GraphQL Types includes these: 645 | 646 | =item1 B (abstract, not to be used directly, only inherited 647 | =item2 B 648 | =item3 B 649 | =item3 B 650 | =item3 B 651 | =item3 B 652 | =item3 B 653 | =item2 B 654 | =item2 B 655 | =item2 B 656 | =item2 B 657 | =item2 B 658 | =item2 B 659 | =item2 B 660 | =item2 B 661 | =item2 B 662 | =item2 B 663 | 664 | =head3 I B 665 | 666 | B and B are B 667 | 668 | They get two extra public attributes C<$.isDeprecated> I, 669 | default C, and C<$.deprecationReason> I. 670 | 671 | They also get the method C<.deprecate(Str $reason)>, which defaults to 672 | "No longer supported." 673 | 674 | In I, you can also deprecate with the directive B<@deprecate> or 675 | C<@deprecate(reason: "something")>. More on directives below. 676 | 677 | =head3 I B 678 | 679 | B and B both include a role 680 | B that give them a B<@.fieldlist> array of 681 | Bs, a method B<.field($name)> to look up a field, and 682 | a method B<.fields(Bool :$includeDeprecated)> that will return the 683 | list of fields. Meta-fields with names starting with "__" are 684 | explicitly not returned in the C<.fields()> list, but can be requested 685 | with C<.field()>. 686 | 687 | =head3 B 688 | 689 | This is the main GraphQL type base class. It has public attributes 690 | C<$.name> and C<$.description>. It isn't intended to be used 691 | directly, it is just the base class for all the other Types. 692 | 693 | The description field can be explicitly assigned in the creation of 694 | each GraphQL::Type. 695 | 696 | In I, you can set the description field by preceding the 697 | definition of types with comments: 698 | 699 | # Description for mytype 700 | type mytype { 701 | # Description for myfield 702 | myfield: Str 703 | } 704 | 705 | In Perl, the description field is set from the Meto-Object Protocol 706 | $obj.WHY method which by default will be set automatically with Pod 707 | declarations. e.g. 708 | 709 | #| Description for mytype 710 | class mytype { 711 | #| Description for myfield 712 | has Str $.myfield 713 | } 714 | 715 | =head3 B is B 716 | 717 | Serves as the base class for scalar, leaf types. It adds the method 718 | B<.kind()> = 'SCALAR'; 719 | 720 | There are several core GraphQL scalar types that map to Perl basic 721 | scalar types: 722 | 723 | =begin table 724 | GraphQL Type | Perl Type Class | Perl Object Instance | Perl Type 725 | =========================================================================== 726 | String | GraphQL::String | GraphQLString | Str 727 | 728 | Int | GraphQL::Int | GraphQLInt | Int 729 | 730 | Float | GraphQL::Float | GraphQLFloat | Num 731 | 732 | Boolean | GraphQL::Boolean | GraphQLBoolean | Bool 733 | 734 | ID | GraphQL::ID | GraphQLID | ID (subset of Cool) 735 | ---------------------------------------------------------------------------- 736 | =end table 737 | 738 | The Perl Object Instances are just short hand pre-created objects that 739 | can be used since those types are needed so frequently. 740 | 741 | For example, GraphQL::String.new creates a String type, but you can 742 | just use GraphQLString which is already made. 743 | 744 | You can create your own additional scalar types as needed: 745 | 746 | my $URL = GraphQL::Scalar.new(name => 'URL'); 747 | 748 | or in I: 749 | 750 | scalar URL 751 | 752 | =head4 B is B 753 | 754 | Core String type, maps to Perl type C. 755 | 756 | You can create your own: 757 | 758 | my $String = GraphQL::String.new; 759 | 760 | or just use C. 761 | 762 | =head4 B is B 763 | 764 | Core Int type, maps to Perl type C. 765 | 766 | You can create your own: 767 | 768 | my $Int = GraphQL::Int.new; 769 | 770 | or just use C. 771 | 772 | =head4 B is B 773 | 774 | Core Float type, maps to Perl type C. 775 | 776 | You can create your own: 777 | 778 | my $Float = GraphQL::Float.new; 779 | 780 | or just use C. 781 | 782 | =head4 B is B 783 | 784 | Core Boolean type, maps to Perl type C. 785 | 786 | You can create your own: 787 | 788 | my $Boolean = GraphQL::Boolean.new; 789 | 790 | or just use C. 791 | 792 | =head4 B is B 793 | 794 | Core ID type, maps to Perl type C which is a subset of C. 795 | 796 | You can create your own: 797 | 798 | my $ID = GraphQL::ID.new; 799 | 800 | or just use C. 801 | 802 | =head4 B is B does B 803 | 804 | The individual enumerated values of an C, represented as quoted 805 | strings in JSON. 806 | 807 | my $enumvalue = GraphQL::EnumValue.new(name => 'SOME_VALUE'); 808 | 809 | They can also be deprecated: 810 | 811 | my $enumvalue = GraphQL::EnumValue.new(name => 'SOME_VALUE', 812 | :isDeprecated, 813 | reason => 'Just because'); 814 | 815 | or can be later deprecated: 816 | 817 | $enumvalue.deprecate('Just because'); 818 | 819 | See B for more information about creating EnumValues. 820 | 821 | =head4 B is B 822 | 823 | B<.kind()> = 'LIST', and has B<$.ofType> with some other 824 | GraphQL::Type. 825 | 826 | my $list-of-strings = GraphQL::List.new(ofType => GraphQLString); 827 | 828 | In I, Lists are represented by wrapping another type with square 829 | brackets '[' and ']'. e.g. 830 | 831 | [String] 832 | 833 | =head4 B is B 834 | 835 | By default GraphQL types can all take on the value C (in Perl, 836 | C). Wrapping them with Non-Null disallows the C. 837 | 838 | B<.kind()> = 'NON_NULL' 839 | 840 | my $non-null-string = GraphQL::Non-Null.new(ofType => GraphQLString); 841 | 842 | In I, Non-Null types are represented by appending an exclation 843 | point, '!'. e.g. 844 | 845 | String! 846 | 847 | To define a Perl class with a non-null attribute, both add the C<:D> 848 | type constraint to the type, and also specify it as C (or 849 | give it a default). To mark a type in a method as non-null, append 850 | with an exclamation point. e.g. 851 | 852 | class Something 853 | { 854 | has Str:D $.my is rw is required; 855 | 856 | method something(Str :$somearg! --> ID) { ... } 857 | } 858 | 859 | =head4 B is B 860 | 861 | The type is used to represent arguments for Bs and 862 | Bs arguments as well as the C of a 863 | B. Has a C<$.type> attribute and optionally 864 | a C<$.defaultValue> attribute. 865 | 866 | my $inputvalue = GraphQL::InputValue.new(name => 'somearg', 867 | type => GraphQLString, 868 | defaultValue => 'some default'); 869 | 870 | in I: 871 | 872 | somearg: String = "some default" 873 | 874 | in Perl: 875 | 876 | Str :$somearg = 'some default' 877 | 878 | =head4 B is B does B 879 | 880 | In addition to the inherited B<.name>, B<.description>, 881 | B<.isDeprecated>, B<.deprecationReason>, has attributes B<.args> which 882 | is an array of Bs, and B<.type> which is the type 883 | of this field. Since the Field is the place where the Schema connects 884 | to resolvers, there is also a B<.resolver> attribute which can be 885 | connected to arbitrary code. Much more about resolvers in Resolution 886 | below. 887 | 888 | my $field = GraphQL::Field.new( 889 | name => 'myfield', 890 | type => GraphQLString, 891 | args => GraphQL::InputValue.new( 892 | name => 'somearg', 893 | type => GraphQLString, 894 | defaultValue => 'some default'), 895 | resolver => sub { ... }); 896 | 897 | In I: 898 | 899 | myfield(somearg: String = "some default"): String 900 | 901 | In Perl: 902 | 903 | method myfield(Str :$somearg = 'some default' --> Str) { ... } 904 | 905 | Note that as a strongly, staticly typed system, every argument must be 906 | a named argument, and have an attached type (a valid one in the list 907 | above that map to GraphQL types), and the return must specify a type. 908 | 909 | You can deprecate by setting the attributes B<.isDeprecated> and 910 | optionally B<.deprecationReason> or using the I B<@deprecate> 911 | directive described below. 912 | 913 | =head4 B is B does B 914 | 915 | In addition to the inherited B<$.name>, B<$.description>, and 916 | B<@.fieldlist>, also has the attribute B<@.possibleTypes> with the 917 | list of object types that implement the interface. You needn't set 918 | B<@.possibleTypes>, as each B specifies which 919 | interfaces they implement, and the Schema finalization will list them 920 | all here. 921 | 922 | my $interface = GraphQL::Interface.new( 923 | name => 'myinterface', 924 | fieldlist => (GraphQL::Field.new(...), GraphQL::Field.new(...)) 925 | ); 926 | 927 | In I: 928 | 929 | interface myinterface { 930 | ...fields... 931 | } 932 | 933 | =head4 B is B does B 934 | 935 | In addition to the inherited B<$.name>, B<$.description>, and 936 | B<@.fieldlist>, also has the attribute B<@.interfaces> with the 937 | interfaces which the object implements, and the B<.kind()> method 938 | which always returns 'OBJECT'. 939 | 940 | my $obj = GraphQL::Object.new( 941 | name => 'myobject', 942 | interfaces => ($someinterface, $someotherinterface), 943 | fieldlist => (GraphQL::Field.new(...), GraphQL::Field.new(...)) 944 | ); 945 | 946 | In I: 947 | 948 | type myobject implements someinterface, someotherinterface { 949 | ...fields... 950 | } 951 | 952 | In Perl: 953 | 954 | class myobject { 955 | ...fields... 956 | } 957 | 958 | NOTE: Interfaces aren't yet implemented for the perl classes. 959 | 960 | =head4 B is B 961 | 962 | Input Objects are object like types used as inputs to queries. Their 963 | B<.kind()> method returns 'INPUT_OBJECT'. They have a 964 | B<@.inputFields> array of Bs, very similar to the 965 | fields defined within a normal Object. 966 | 967 | my $obj = GraphQL::Input.new( 968 | name => 'myinputobject', 969 | inputFields => (GraphQL::InputValue.new(...), GraphQL::InputValue.new(...) 970 | ); 971 | 972 | In I: 973 | 974 | input myinputobject { 975 | ...inputvalues... 976 | } 977 | 978 | In Perl, you must specify a class explicitly as a GraphQL::InputObject: 979 | 980 | class myinputobject is GraphQL::InputObject { 981 | ...inputvalues... 982 | } 983 | 984 | =head4 B is B 985 | 986 | A union has B<.kind()> = 'UNION', and a B<@.possibleTypes> attribute 987 | listing the types of the union. 988 | 989 | my $union = GraphQL::Union.new( 990 | name => 'myunion', 991 | possibleTypes => ($someobject, $someotherobject) 992 | ); 993 | 994 | In I: 995 | 996 | union myunion = someobject | someotherobject 997 | 998 | NOTE: Not yet implemented in Perl classes. 999 | 1000 | =head4 B is B 1001 | 1002 | Has B<.kind()> = 'ENUM', and B<@.enumValues> with a list of 1003 | Bs. The accessor method for B<.enumValues()> 1004 | takes an optional I argument C<:$includeDeprecated> which will 1005 | either include deprecated values or exclude them. 1006 | 1007 | my $enum = GraphQL::Enum.new( 1008 | name => 'myenum', 1009 | enumValues => (GraphQL::EnumValue.new(...), GraphQL::EnumValue.new(...)) 1010 | ); 1011 | 1012 | In I: 1013 | 1014 | enum myenum { VAL1 VAL2 ... } 1015 | 1016 | In Perl: 1017 | 1018 | enum myenum ; 1019 | 1020 | =head4 B is B 1021 | 1022 | Still needs work... 1023 | 1024 | =end pod 1025 | -------------------------------------------------------------------------------- /lib/GraphQL/Actions.pm: -------------------------------------------------------------------------------- 1 | use GraphQL::Types; 2 | 3 | unit class GraphQL::Actions; 4 | # 5 | # There are two "top level" rules, for a GraphQL query document, 6 | # and for a GraphQL type schema. 7 | # 8 | 9 | has GraphQL::Document $!q = GraphQL::Document.new; 10 | 11 | has $.schema; 12 | has GraphQL::Type @!newtypes; 13 | 14 | my %ESCAPE = ( 15 | '"' => '"', 16 | '\\' => '\\', 17 | '/' => '/', 18 | 'b' => "\b", 19 | 'f' => "\f", 20 | 'n' => "\n", 21 | 'r' => "\r", 22 | 't' => "\t" 23 | ); 24 | 25 | method Document($/) 26 | { 27 | if $!q.operations{''} and $!q.operations.elems() != 1 28 | { 29 | die "This anonymous operation must be the only defined operation"; 30 | } 31 | 32 | make $!q; 33 | } 34 | 35 | method OperationDefinition($/) 36 | { 37 | my $name = $ ?? $.made !! ''; 38 | 39 | die "Duplicate definition of $name" if $!q.operations{$name}.defined; 40 | 41 | $!q.operations{$name} = GraphQL::Operation.new( 42 | name => $name, 43 | operation => $ ?? $.Str !! 'query', 44 | vars => $.made // (), 45 | selectionset => $.made 46 | ); 47 | } 48 | 49 | method VariableDefinitions($/) 50 | { 51 | make $».made; 52 | } 53 | 54 | method VariableDefinition($/) 55 | { 56 | make GraphQL::Variable.new( 57 | name => $..made, 58 | type => $.made, 59 | defaultValue => $.made 60 | ); 61 | } 62 | 63 | method SelectionSet($/) 64 | { 65 | make $».made 66 | } 67 | 68 | method Selection($/) 69 | { 70 | make $.made // $.made // $.made; 71 | } 72 | 73 | method QueryField($/) 74 | { 75 | make GraphQL::QueryField.new( 76 | alias => $.made, 77 | name => $.made, 78 | args => $.made // (), 79 | directives => $.made // (), 80 | selectionset => $.made // () 81 | ); 82 | } 83 | 84 | method Alias($/) 85 | { 86 | make $.made; 87 | } 88 | 89 | method Arguments($/) 90 | { 91 | my %args; 92 | for $ -> $arg 93 | { 94 | %args{$arg.made} = $arg.made; 95 | } 96 | make %args; 97 | } 98 | 99 | method FragmentSpread($/) 100 | { 101 | make GraphQL::FragmentSpread.new( 102 | name => $.made, 103 | directives => $.made // () 104 | ); 105 | } 106 | 107 | method InlineFragment($/) 108 | { 109 | make GraphQL::InlineFragment.new( 110 | onType => $.made, 111 | directives => $.made // (), 112 | selectionset => $.made 113 | ); 114 | } 115 | 116 | method FragmentDefinition($/) 117 | { 118 | $!q.fragments{$.made} = GraphQL::Fragment.new( 119 | name => $.made, 120 | onType => $.made, 121 | directives => $.made // (), 122 | selectionset => $.made 123 | ); 124 | } 125 | 126 | method FragmentName($/) 127 | { 128 | make $.Str; 129 | } 130 | 131 | method TypeCondition($/) 132 | { 133 | make $.made; 134 | } 135 | 136 | method Name($/) 137 | { 138 | make $/.Str; 139 | } 140 | 141 | method Variable($/) 142 | { 143 | make GraphQL::Variable.new(name => $.made); 144 | } 145 | 146 | method Value:sym($/) 147 | { 148 | make $.made; 149 | } 150 | 151 | method Value:sym($/) 152 | { 153 | make $/.Num; 154 | } 155 | 156 | method Value:sym($/) 157 | { 158 | make $/.Int; 159 | } 160 | 161 | method StringValue($/) 162 | { 163 | make $».made.join; 164 | } 165 | 166 | method str($/) 167 | { 168 | make ~$/; 169 | } 170 | 171 | # copied string escape from JSON::Tiny 172 | 173 | my %h = '\\' => "\\", 174 | '/' => "/", 175 | 'b' => "\b", 176 | 'n' => "\n", 177 | 't' => "\t", 178 | 'f' => "\f", 179 | 'r' => "\r", 180 | '"' => "\""; 181 | 182 | method str_escape($/) 183 | { 184 | if $ 185 | { 186 | make utf16.new( $.map({:16(~$_)}) ).decode(); 187 | } 188 | else 189 | { 190 | make %h{~$/}; 191 | } 192 | } 193 | 194 | method Value:sym($/) 195 | { 196 | make $.made; 197 | } 198 | 199 | method Value:sym($/) 200 | { 201 | make $/.Str eq 'true' ?? True !! False; 202 | } 203 | 204 | method Value:sym($/) 205 | { 206 | make Nil 207 | } 208 | 209 | method Value:sym($/) 210 | { 211 | make $.made; 212 | } 213 | 214 | method ObjectValue($/) 215 | { 216 | make %( $».made ); 217 | } 218 | 219 | method ObjectField($/) 220 | { 221 | make $.made => $.made; 222 | } 223 | 224 | method Value:sym($/) 225 | { 226 | make $.made; 227 | } 228 | 229 | method Type($/) 230 | { 231 | make $.made // $.made // $.made; 232 | } 233 | 234 | method NamedType($/) 235 | { 236 | make $!schema.type($.Str); 237 | } 238 | 239 | method ListType($/) 240 | { 241 | make GraphQL::List.new(ofType => $.made); 242 | } 243 | 244 | method NonNullType($/) 245 | { 246 | my $type = $.made || $.made; 247 | 248 | make GraphQL::Non-Null.new(ofType => $type); 249 | } 250 | 251 | method Interface($/) 252 | { 253 | my $i = GraphQL::Interface.new(name => $.made, 254 | fieldlist => $.made); 255 | 256 | $i.add-comment-description($/); 257 | 258 | push @!newtypes, $i; 259 | make $i; 260 | } 261 | 262 | method FieldList($/) 263 | { 264 | make $».made; 265 | } 266 | 267 | method Comment($/) 268 | { 269 | make $/.Str.subst(/^\#\s?/, ''); 270 | } 271 | 272 | method Field($/) 273 | { 274 | my $f = GraphQL::Field.new( 275 | name => $.made, 276 | args => $.made // (), 277 | type => $.made 278 | ); 279 | 280 | $f.add-comment-description($/); 281 | 282 | if $.made:exists 283 | { 284 | if $.made:exists 285 | { 286 | $f.deprecate($.made); 287 | } 288 | else 289 | { 290 | $f.deprecate(); 291 | } 292 | } 293 | 294 | make $f; 295 | } 296 | 297 | method ObjectType($/) 298 | { 299 | my $o = GraphQL::Object.new(name => $.made, 300 | fieldlist => $.made, 301 | interfaces => $.made // ()); 302 | 303 | $o.add-comment-description($/); 304 | 305 | push @!newtypes, $o; 306 | make $o; 307 | } 308 | 309 | method Implements($/) 310 | { 311 | make $.map({ $!schema.type(.made) }); 312 | } 313 | 314 | method Union($/) 315 | { 316 | my $u = GraphQL::Union.new(name => $.made, 317 | possibleTypes => $.made); 318 | 319 | $u.add-comment-description($/); 320 | 321 | push @!newtypes, $u; 322 | make $u; 323 | } 324 | 325 | method UnionList($/) 326 | { 327 | make $.map({ $!schema.type(.made) }); 328 | } 329 | 330 | method Value:sym($/) 331 | { 332 | make $.made 333 | } 334 | 335 | method ListValue($/) 336 | { 337 | make $».made; 338 | } 339 | 340 | method Enum($/) 341 | { 342 | my $e = GraphQL::Enum.new(name => $.made, 343 | enumValues => $.made); 344 | 345 | $e.add-comment-description($/); 346 | 347 | push @!newtypes, $e; 348 | make $e; 349 | } 350 | 351 | method EnumValues($/) 352 | { 353 | make $».made; 354 | } 355 | 356 | method EnumValue($/) 357 | { 358 | my $enumvalue = GraphQL::EnumValue.new(name => $.made); 359 | 360 | $enumvalue.add-comment-description($/); 361 | 362 | if $.made:exists 363 | { 364 | if $.made:exists 365 | { 366 | $enumvalue.deprecate($.made); 367 | } 368 | else 369 | { 370 | $enumvalue.deprecate(); 371 | } 372 | } 373 | make $enumvalue; 374 | } 375 | 376 | method Directives($/) 377 | { 378 | my %directives; 379 | for $ -> $directive 380 | { 381 | %directives{$directive.made} = $directive.made // (); 382 | } 383 | make %directives; 384 | } 385 | 386 | method DefaultValue($/) 387 | { 388 | make $.made; 389 | } 390 | 391 | method ArgumentDefinition($/) 392 | { 393 | make GraphQL::InputValue.new(name => $.made, 394 | type => $.made, 395 | defaultValue => $.made); 396 | } 397 | 398 | method ArgumentDefinitions($/) 399 | { 400 | make $».made; 401 | } 402 | 403 | method Scalar($/) 404 | { 405 | my $o = GraphQL::Scalar.new(name => $.made); 406 | 407 | $o.add-comment-description($/); 408 | 409 | push @!newtypes, $o; 410 | make $o; 411 | } 412 | 413 | method InputObject($/) 414 | { 415 | my $o = GraphQL::Input.new(name => $.made, 416 | inputFields => $.made); 417 | 418 | $o.add-comment-description($/); 419 | 420 | push @!newtypes, $o; 421 | make $o; 422 | } 423 | 424 | method InputFieldList($/) 425 | { 426 | make $».made; 427 | } 428 | 429 | method InputField($/) 430 | { 431 | my $f = GraphQL::InputValue.new(name => $.made, 432 | type => $.made, 433 | defaultValue => $.made); 434 | 435 | $f.add-comment-description($/); 436 | 437 | make $f; 438 | } 439 | 440 | method TypeSchema($/) 441 | { 442 | $!schema.add-type(@!newtypes); 443 | 444 | make $!schema; 445 | } 446 | 447 | method Schema($/) 448 | { 449 | $!schema.query = $.made; 450 | $!schema.mutation = $.made; 451 | } 452 | 453 | method SchemaQuery($/) 454 | { 455 | make $.made; 456 | } 457 | 458 | method SchemaMutation($/) 459 | { 460 | make $.made; 461 | } 462 | -------------------------------------------------------------------------------- /lib/GraphQL/Compare.pm: -------------------------------------------------------------------------------- 1 | unit module GraphQL::Compare; 2 | 3 | use GraphQL::Types; 4 | 5 | CORE::<&infix:>.add_dispatchee( 6 | multi infix:(GraphQL::Field $l, GraphQL::Field $r --> Bool) 7 | { 8 | $l.name eqv $r.name and 9 | $l.description eqv $r.description and 10 | $l.type eqv $r.type; 11 | }); 12 | 13 | CORE::<&infix:>.add_dispatchee( 14 | multi infix:(GraphQL::Interface $l, GraphQL::Interface $r --> Bool) 15 | { 16 | $l.name eqv $r.name and 17 | $l.description eqv $r.description and 18 | $l.fieldlist eqv $r.fieldlist and 19 | $l.possibleTypes».name eqv $r.possibleTypes».name; 20 | }); 21 | 22 | CORE::<&infix:>.add_dispatchee( 23 | multi infix:(GraphQL::Object $l, GraphQL::Object $r --> Bool) 24 | { 25 | $l.name eqv $r.name and 26 | $l.description eqv $r.description and 27 | $l.fieldlist eqv $r.fieldlist and 28 | $l.interfaces».name eqv $r.interfaces».name; 29 | }); 30 | 31 | CORE::<&infix:>.add_dispatchee( 32 | multi infix:(GraphQL::Union $l, GraphQL::Union $r --> Bool) 33 | { 34 | $l.name eqv $r.name and 35 | $l.description eqv $r.description and 36 | $l.possibleTypes».name eqv $r.possibleTypes».name; 37 | }); 38 | -------------------------------------------------------------------------------- /lib/GraphQL/Execution.pm: -------------------------------------------------------------------------------- 1 | unit module GraphQL::Execution; 2 | 3 | use GraphQL::Types; 4 | use GraphQL::Response; 5 | 6 | my Set $background-methods = Set.new; 7 | 8 | multi sub trait_mod:(Method $m, :$graphql-background!) is export 9 | { 10 | $background-methods ∪= $m; 11 | } 12 | 13 | sub ExecuteRequest(:$document, 14 | Str :$operationName, 15 | :%variables, 16 | :$initialValue, 17 | :$schema, 18 | :%session) is export 19 | { 20 | my $operation = $document.GetOperation($operationName); 21 | 22 | my $selectionSet = $operation.selectionset; 23 | 24 | my %coercedVariableValues = CoerceVariableValues(:$operation, 25 | :%variables); 26 | 27 | my $objectValue = $initialValue // 0; 28 | 29 | my $objectType = $operation.operation eq 'mutation' 30 | ?? $schema.mutationType 31 | !! $schema.queryType; 32 | 33 | ExecuteSelectionSet(:$selectionSet, 34 | :$objectType, 35 | :$objectValue, 36 | :%variables, 37 | :$document, 38 | :%session); 39 | } 40 | 41 | sub ExecuteSelectionSet(:@selectionSet, 42 | GraphQL::Object :$objectType, 43 | :$objectValue! is rw, 44 | :%variables, 45 | GraphQL::Document :$document, 46 | :%session) 47 | { 48 | my @groupedFieldSet = CollectFields(:$objectType, 49 | :@selectionSet, 50 | :%variables, 51 | :$document); 52 | my @results; 53 | 54 | for @groupedFieldSet -> $p 55 | { 56 | my $responseKey = $p.key; 57 | my @fields = |$p.value; 58 | 59 | my $fieldName = @fields[0].name; 60 | 61 | my $responseValue; 62 | 63 | my $fieldType = $objectType.field($fieldName).type 64 | or die qq{Cannot query field '$fieldName' } ~ 65 | qq{on type '$objectType.name()'.}; 66 | 67 | $responseValue = ExecuteField(:$objectType, 68 | :$objectValue, 69 | :@fields, 70 | :$fieldType, 71 | :%variables, 72 | :$document, 73 | :%session); 74 | 75 | my $type = $fieldType ~~ GraphQL::Interface | GraphQL::Union 76 | ?? GraphQL::Object 77 | !! $fieldType; 78 | 79 | push @results, GraphQL::Response.new(:$type, 80 | name => $responseKey, 81 | value => $responseValue); 82 | } 83 | 84 | return @results; 85 | } 86 | 87 | sub ExecuteField(GraphQL::Object :$objectType, 88 | :$objectValue! is rw, 89 | :@fields, 90 | GraphQL::Type :$fieldType, 91 | :%variables, 92 | :$document, 93 | :%session) 94 | { 95 | my $field = @fields[0]; 96 | 97 | my $fieldName = $field.name; 98 | 99 | my %argumentValues = CoerceArgumentValues(:$objectType, 100 | :$field, 101 | :%variables); 102 | 103 | my $resolvedValue = ResolveFieldValue(:$objectType, 104 | :$objectValue, 105 | :$fieldName, 106 | :%argumentValues, 107 | :%session); 108 | 109 | if $resolvedValue ~~ Promise 110 | { 111 | return $resolvedValue.then( 112 | { 113 | CompleteValue(:$fieldType, 114 | :@fields, 115 | :result($resolvedValue.result), 116 | :%variables, 117 | :$document) 118 | }); 119 | } 120 | else 121 | { 122 | return CompleteValue(:$fieldType, 123 | :@fields, 124 | :result($resolvedValue), 125 | :%variables, 126 | :$document); 127 | } 128 | } 129 | 130 | sub CompleteValue(GraphQL::Type :$fieldType, 131 | :@fields, 132 | :$result, 133 | :%variables, 134 | :$document) 135 | { 136 | given $fieldType 137 | { 138 | when GraphQL::Enum 139 | { 140 | return $fieldType.valid($result) ?? $result !! Nil; 141 | } 142 | 143 | when GraphQL::Scalar 144 | { 145 | return $result; 146 | } 147 | 148 | when GraphQL::Non-Null 149 | { 150 | my $completedResult = CompleteValue(:fieldType($fieldType.ofType), 151 | :@fields, 152 | :$result, 153 | :%variables, 154 | :$document); 155 | 156 | die "Null in non-null type" unless $completedResult.defined; 157 | 158 | return $completedResult; 159 | } 160 | 161 | return unless $result.defined; 162 | 163 | when GraphQL::List 164 | { 165 | die "Must return a List" unless $result ~~ List | Seq; 166 | 167 | my $list = $result.map({ CompleteValue( 168 | :fieldType($fieldType.ofType), 169 | :@fields, 170 | :result($_), 171 | :%variables, 172 | :$document) }); 173 | return $list; 174 | } 175 | 176 | when GraphQL::Object | GraphQL::Interface | GraphQL::Union 177 | { 178 | my $objectType = $fieldType ~~ GraphQL::Object 179 | ?? $fieldType 180 | !! ResolveAbstractType(:$fieldType, :$result); 181 | 182 | my @subSelectionSet = MergeSelectionSets(:@fields); 183 | 184 | my $objectValue = $result; 185 | 186 | return ExecuteSelectionSet(:selectionSet(@subSelectionSet), 187 | :$objectType, 188 | :$objectValue, 189 | :%variables, 190 | :$document); 191 | } 192 | 193 | default 194 | { 195 | die "Complete Value Unknown Type"; 196 | } 197 | } 198 | } 199 | 200 | sub ResolveAbstractType(:$fieldType, :$result) 201 | { 202 | $fieldType.possibleTypes.first: { .name eq $result.WHAT.^name } 203 | } 204 | 205 | sub MergeSelectionSets(:@fields) 206 | { 207 | my @list; 208 | 209 | for @fields -> $field 210 | { 211 | for $field.selectionset -> $sel 212 | { 213 | push @list, $sel; 214 | } 215 | } 216 | 217 | return @list; 218 | } 219 | 220 | sub CoerceVariableValues(GraphQL::Operation :$operation, 221 | :%variables) 222 | { 223 | my %coercedValues; 224 | 225 | for $operation.vars -> $v 226 | { 227 | %coercedValues{$v.name} = $v.type.coerce(%variables{$v.name} 228 | // $v.defaultValue); 229 | } 230 | 231 | return %coercedValues; 232 | } 233 | 234 | sub ReplaceVariable(:$value, :%variables) 235 | { 236 | given $value 237 | { 238 | when GraphQL::Variable 239 | { 240 | %variables{$value.name}:exists 241 | ?? %variables{$value.name} 242 | !! Nil; 243 | } 244 | when Hash 245 | { 246 | Hash.new: 247 | do for $value.kv -> $k, $v 248 | { 249 | $k => ReplaceVariable(value => $v, :%variables) 250 | } 251 | } 252 | when List 253 | { 254 | Array.new: $value.map({ ReplaceVariable(value => $_, :%variables) }) 255 | } 256 | default 257 | { 258 | $value 259 | } 260 | } 261 | } 262 | 263 | sub CoerceArgumentValues(GraphQL::Object :$objectType, 264 | GraphQL::QueryField :$field, 265 | :%variables) 266 | { 267 | my %coercedValues; 268 | 269 | for $objectType.field($field.name).args -> $arg 270 | { 271 | my $value = $field.args{$arg.name}; 272 | 273 | $value = ReplaceVariable(:$value, :%variables); 274 | 275 | $value //= $arg.defaultValue; 276 | 277 | %coercedValues{$arg.name} = $arg.type.coerce($value); 278 | } 279 | 280 | return %coercedValues; 281 | } 282 | 283 | sub CollectFields(GraphQL::Object :$objectType, 284 | :@selectionSet, 285 | :%variables, 286 | :$visitedFragments is copy = ∅, 287 | GraphQL::Document :$document) 288 | { 289 | my %groupedFields; 290 | my @responsekeys; 291 | 292 | for @selectionSet -> $selection 293 | { 294 | if $selection.directives 295 | { 296 | given $selection.directives 297 | { 298 | when Bool 299 | { 300 | next if $_; 301 | } 302 | when GraphQL::Variable and $_.type ~~ GraphQL::Boolean 303 | { 304 | next if %variables{$_.name}; 305 | } 306 | } 307 | } 308 | 309 | if $selection.directives 310 | { 311 | given $selection.directives 312 | { 313 | when Bool 314 | { 315 | next unless $_; 316 | } 317 | when GraphQL::Variable and $_.type ~~ GraphQL::Boolean 318 | { 319 | next unless %variables{$_.name}; 320 | } 321 | } 322 | } 323 | 324 | given $selection 325 | { 326 | when GraphQL::QueryField 327 | { 328 | unless %groupedFields{$selection.responseKey}:exists 329 | { 330 | %groupedFields{$selection.responseKey} = []; 331 | push @responsekeys, $selection.responseKey; 332 | } 333 | 334 | push %groupedFields{$selection.responseKey}, $selection; 335 | } 336 | 337 | when GraphQL::FragmentSpread 338 | { 339 | my $fragmentSpreadName = $selection.name; 340 | 341 | next if $fragmentSpreadName ∈ $visitedFragments; 342 | 343 | $visitedFragments ∪= $fragmentSpreadName; 344 | 345 | my $fragment = $document.fragments{$fragmentSpreadName} or next; 346 | 347 | my $fragmentType = $fragment.onType; 348 | 349 | next unless $objectType.fragment-applies($fragmentType); 350 | 351 | my @fragmentSelectionSet = $fragment.selectionset; 352 | 353 | my @fragmentGroupedFieldSet = CollectFields( 354 | :$objectType, 355 | :selectionSet(@fragmentSelectionSet), 356 | :%variables, 357 | :$visitedFragments, 358 | :$document); 359 | 360 | for @fragmentGroupedFieldSet -> $p 361 | { 362 | my $responseKey = $p.key; 363 | my @fragmentGroup = |$p.value; 364 | 365 | unless %groupedFields{$responseKey}:exists 366 | { 367 | %groupedFields{$responseKey} = []; 368 | push @responsekeys, $responseKey; 369 | } 370 | push %groupedFields{$responseKey}, |@fragmentGroup; 371 | } 372 | } 373 | 374 | when GraphQL::InlineFragment 375 | { 376 | my $fragmentType = $selection.onType; 377 | 378 | next if $fragmentType.defined and 379 | not $objectType.fragment-applies($fragmentType); 380 | 381 | my @fragmentSelectionSet = $selection.selectionset; 382 | 383 | my @fragmentGroupedFieldSet = CollectFields( 384 | :$objectType, 385 | :selectionSet(@fragmentSelectionSet), 386 | :%variables, 387 | :$visitedFragments, 388 | :$document); 389 | 390 | for @fragmentGroupedFieldSet -> $p 391 | { 392 | my $responseKey = $p.key; 393 | my @fragmentGroup = |$p.value; 394 | 395 | unless %groupedFields{$responseKey}:exists 396 | { 397 | %groupedFields{$responseKey} = []; 398 | push @responsekeys, $responseKey; 399 | } 400 | 401 | push %groupedFields{$responseKey}, |@fragmentGroup; 402 | } 403 | } 404 | } 405 | } 406 | 407 | return @responsekeys.map( { $_ => %groupedFields{$_} } ); 408 | } 409 | 410 | sub ResolveArgs(Signature $sig, *%allargs) 411 | { 412 | my %args; 413 | 414 | for $sig.params -> $p 415 | { 416 | if ($p.named) 417 | { 418 | for $p.named_names -> $param_name 419 | { 420 | if %allargs{$param_name}:exists 421 | { 422 | %args{$param_name} = %allargs{$param_name}; 423 | last; 424 | } 425 | } 426 | } 427 | } 428 | 429 | return %args; 430 | } 431 | 432 | sub ResolveFieldValue(GraphQL::Object :$objectType, 433 | :$objectValue!, 434 | :$fieldName, 435 | :%argumentValues, 436 | :%session) 437 | { 438 | my $field = $objectType.field($fieldName) or return; 439 | 440 | if $field.resolver 441 | { 442 | my $args = ResolveArgs($field.resolver.signature, 443 | :$objectValue, 444 | |%argumentValues, 445 | |%session); 446 | 447 | if $field.resolver ~~ Sub 448 | { 449 | $field.resolver.(|$args); 450 | } 451 | elsif $field.resolver ~~ Method 452 | { 453 | if ($objectValue) 454 | { 455 | if $field.resolver ∈ $background-methods 456 | { 457 | start $objectValue."$fieldName"(|$args) 458 | } 459 | else 460 | { 461 | $objectValue."$fieldName"(|$args) 462 | } 463 | } 464 | else 465 | { 466 | if $field.resolver ∈ $background-methods 467 | { 468 | start $field.resolver.package."$fieldName"(|$args) 469 | } 470 | else 471 | { 472 | $field.resolver.package."$fieldName"(|$args) 473 | } 474 | } 475 | } 476 | } 477 | elsif $objectValue ~~ Hash and $objectValue{$fieldName}:exists 478 | { 479 | $objectValue{$fieldName}; 480 | } 481 | elsif $objectValue.^lookup($fieldName) -> $method 482 | { 483 | $objectValue."$fieldName"(|ResolveArgs($method.signature, 484 | :$objectValue, 485 | |%argumentValues)) 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /lib/GraphQL/Grammar.pm: -------------------------------------------------------------------------------- 1 | #use Grammar::Tracer; 2 | 3 | unit grammar GraphQL::Grammar; 4 | 5 | # 6 | # Adapted expect() and error() from 7 | # https://perlgeek.de/blog-en/perl-6/2017-007-book-parse-errors.html 8 | # 9 | 10 | method expect($what) 11 | { 12 | self.error("expected $what"); 13 | } 14 | 15 | method error($msg) 16 | { 17 | my $parsed-so-far = self.target.substr(0, self.pos); 18 | my @lines = $parsed-so-far.lines; 19 | die "Parse failure: $msg at line @lines.elems()" ~ 20 | ("after '@lines[*-1]'" if @lines.elems > 1); 21 | } 22 | 23 | token SourceCharacter { <[\x[0009]\x[000A]\x[000D]\x[0020]..\x[FFFF]]> } 24 | 25 | token Ignored 26 | { | | | } 27 | 28 | token UnicodeBOM { \x[FEFF] } 29 | 30 | token WhiteSpace { \x[0009] | \x[0020] } 31 | 32 | token LineTerminator 33 | { \x[000A] | \x[000D] | \x[000D]\x[000A] } 34 | 35 | token Comment { '#' <.CommentChar>* } 36 | 37 | token CommentChar { > } 38 | 39 | token Comma { ',' } 40 | 41 | # Lexical Tokens 42 | 43 | token Name { <[_A..Za..z]><[_0..9A..Za..z]>* } 44 | 45 | token IntValue { <.IntegerPart> } 46 | 47 | token IntegerPart 48 | { [ <.NegativeSign>? 0 | <.NegativeSign>? <.NonZeroDigit> <.Digit>* ] } 49 | 50 | token NegativeSign { '-' } 51 | 52 | token Digit { <[0..9]> } 53 | 54 | token NonZeroDigit { <[1..9]>} 55 | 56 | token FloatValue 57 | { 58 | <.IntegerPart> <.FractionalPart> | 59 | <.IntegerPart> <.ExponentPart> | 60 | <.IntegerPart> <.FractionalPart> <.ExponentPart> 61 | } 62 | 63 | token FractionalPart { '.' <.Digit>+ } 64 | 65 | token ExponentPart { <.ExponentIndicator> <.Sign>? <.Digit>+ } 66 | 67 | token ExponentIndicator { [ 'e' | 'E' ] } 68 | 69 | token Sign { [ '+' | '-' ] } 70 | 71 | # copied string stuff from JSON::Tiny 72 | 73 | token StringValue { '"' ~ '"' [ | \\ ]* } 74 | 75 | token str { <-["\\\t\r\n]>+ } 76 | 77 | token str_escape { <["\\/bfnrt]> | 'u' + % '\u' } 78 | 79 | token utf16_codepoint { <.xdigit>**4 } 80 | 81 | # Query Document 82 | 83 | token ws { <.Ignored>* } 84 | 85 | rule Document { <.ws> <.Comment>* % <.ws> + } 86 | 87 | rule Definition { <.Comment>* % <.ws> 88 | [ | ] } 89 | 90 | rule OperationDefinition 91 | { 92 | | 93 | ? ? ? 94 | } 95 | 96 | token OperationType { 'query' | 'mutation' } 97 | 98 | rule SelectionSet { '{' [ <.Comment>* % <.ws> 99 | + 100 | <.Comment>* % <.ws> 101 | '}' || ] } 102 | 103 | rule Selection { | | } 104 | 105 | rule QueryField { ? ? ? ? } 106 | 107 | rule Alias { ':' } 108 | 109 | rule Arguments { '(' + ')' } 110 | 111 | rule Argument { ':' } 112 | 113 | rule FragmentSpread { '...' ? } 114 | 115 | rule InlineFragment { '...' ? ? } 116 | 117 | rule FragmentDefinition 118 | { 119 | 'fragment' ? 120 | } 121 | 122 | rule FragmentName { } 123 | 124 | rule TypeCondition { 'on' } 125 | 126 | proto token Value {*}; 127 | token Value:sym { } 128 | token Value:sym { } 129 | token Value:sym { } 130 | token Value:sym { } 131 | token Value:sym { [ 'true' | 'false' ] } 132 | token Value:sym { 'null' } 133 | token Value:sym { } 134 | token Value:sym { } 135 | token Value:sym { } 136 | 137 | rule ListValue { '[' * % <.ws> ']' } 138 | 139 | rule ObjectValue { '{' * % <.ws> '}' } 140 | 141 | rule ObjectField { ':' } 142 | 143 | rule VariableDefinitions { '(' + % <.ws> ')' } 144 | 145 | rule VariableDefinition { ':' ? } 146 | 147 | rule Variable { '$' } 148 | 149 | rule DefaultValue { '=' } 150 | 151 | rule Type { | | } 152 | 153 | rule NamedType { } 154 | 155 | rule ListType { '[' ']' } 156 | 157 | rule NonNullType { '!' | '!' } 158 | 159 | rule Directives { + % <.ws> } 160 | 161 | rule Directive { '@' ? } 162 | 163 | # Type Schema Language 164 | # Mostly cribbed from 165 | # https://wehavefaces.net/graphql-shorthand-notation-cheatsheet-17cd715861b6 166 | 167 | rule TypeSchema { <.ws> + } 168 | 169 | rule TypeDefinition { | 170 | | 171 | | 172 | | 173 | | 174 | | 175 | } 176 | 177 | rule Interface 178 | { * % <.ws> 'interface' } 179 | 180 | rule FieldList { '{' + '}' } 181 | 182 | rule Field 183 | { * % <.ws> ? ':' ? } 184 | 185 | rule ArgumentDefinitions { '(' + % <.ws> ')' } 186 | 187 | rule ArgumentDefinition { ':' ? } 188 | 189 | rule Scalar 190 | { * % <.ws> 'scalar' } 191 | 192 | rule ObjectType 193 | { * % <.ws> 'type' ? 194 | } 195 | 196 | rule Implements { 'implements' + % <.ws> } 197 | 198 | rule Union 199 | { * % <.ws> 'union' '=' } 200 | 201 | rule UnionList { + % <.UnionSep> } 202 | 203 | rule UnionSep { <.ws> '|' } 204 | 205 | rule Enum 206 | { * % <.ws> 'enum' '{' '}' } 207 | 208 | rule EnumValues { + } 209 | 210 | rule EnumValue { * % <.ws> ? } 211 | 212 | rule InputObject 213 | { * % <.ws> 'input' } 214 | 215 | rule InputFieldList { '{' + '}' } 216 | 217 | rule InputField 218 | { * % <.ws> ':' ? } 219 | 220 | rule Schema { * % <.ws> 221 | 'schema' '{' 222 | ? 223 | ? 224 | ? 225 | '}' } 226 | 227 | rule SchemaQuery { 'query' ':' } 228 | 229 | rule SchemaMutation { 'mutation' ':' } 230 | 231 | rule SchemaSubscription { 'subscription' ':' } 232 | -------------------------------------------------------------------------------- /lib/GraphQL/GraphiQL.pm: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | unit module GraphQL::GraphiQL; 4 | 5 | # This HTML file from Github graphql/graphiql repository 6 | # https://github.com/graphql/graphiql/blob/master/example/index.html 7 | # Redistributed base don the LICENSE file in that repository. 8 | # I changed some links to get graphiql.css and graphiql.js from 9 | # Cloudflare CDN. 10 | 11 | our $GraphiQL is export = Q« 12 | 20 | 21 | 22 | 23 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 52 | 53 | 54 | 55 | 56 | 57 |
Loading...
58 | 160 | 161 | 162 | »; 163 | -------------------------------------------------------------------------------- /lib/GraphQL/Introspection.pm: -------------------------------------------------------------------------------- 1 | unit module GraphQL::Introspection; 2 | 3 | # http://facebook.github.io/graphql/#sec-Introspection 4 | 5 | our $GraphQL-Introspection-Schema is export = Q<< 6 | type __Schema { 7 | types: [__Type!]! 8 | queryType: __Type! 9 | mutationType: __Type 10 | subscriptionType: __Type 11 | directives: [__Directive!]! 12 | } 13 | 14 | type __Type { 15 | kind: __TypeKind! 16 | name: String 17 | description: String 18 | fields(includeDeprecated: Boolean = false): [__Field!] 19 | interfaces: [__Type!] 20 | possibleTypes: [__Type!] 21 | enumValues(includeDeprecated: Boolean = false): [__EnumValue!] 22 | inputFields: [__InputValue!] 23 | ofType: __Type 24 | } 25 | 26 | type __Field { 27 | name: String! 28 | description: String 29 | args: [__InputValue!]! 30 | type: __Type! 31 | isDeprecated: Boolean! 32 | deprecationReason: String 33 | } 34 | 35 | type __InputValue { 36 | name: String! 37 | description: String 38 | type: __Type! 39 | defaultValue: String 40 | } 41 | 42 | type __EnumValue { 43 | name: String! 44 | description: String 45 | isDeprecated: Boolean! 46 | deprecationReason: String 47 | } 48 | 49 | enum __TypeKind { 50 | SCALAR 51 | OBJECT 52 | INTERFACE 53 | UNION 54 | ENUM 55 | INPUT_OBJECT 56 | LIST 57 | NON_NULL 58 | } 59 | 60 | type __Directive { 61 | name: String! 62 | description: String 63 | locations: [__DirectiveLocation!]! 64 | args: [__InputValue!]! 65 | } 66 | 67 | enum __DirectiveLocation { 68 | QUERY 69 | MUTATION 70 | FIELD 71 | FRAGMENT_DEFINITION 72 | FRAGMENT_SPREAD 73 | INLINE_FRAGMENT 74 | ENUM_VALUE 75 | } 76 | >>; 77 | -------------------------------------------------------------------------------- /lib/GraphQL/Response.pm: -------------------------------------------------------------------------------- 1 | use GraphQL::Types; 2 | 3 | unit module GraphQL::Response; 4 | 5 | class GraphQL::Response 6 | { 7 | has Str $.name; 8 | has GraphQL::Type $.type; 9 | has $.value; 10 | 11 | method to-json(Str $indent = '') 12 | { 13 | $!value = await $!value if $!value ~~ Promise; 14 | 15 | $!type.to-json($!name, $!value, $indent); 16 | } 17 | } 18 | 19 | class GraphQL::Error is GraphQL::Response 20 | { 21 | has Str $.message; 22 | has Str @.locations; 23 | 24 | method to-json(Str $indent = '') 25 | { 26 | qq{$indent"message": "$!message"}; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/GraphQL/Types.pm: -------------------------------------------------------------------------------- 1 | unit module GraphQL::Types; 2 | use Text::Wrap; 3 | use JSON::Fast; 4 | 5 | subset ID of Cool is export; 6 | 7 | class GraphQL::InputObject {} 8 | 9 | class GraphQL::Type 10 | { 11 | has Str $.name; 12 | has Str $.description; 13 | 14 | method add-comment-description($/) 15 | { 16 | return unless $; 17 | $!description = $».made.join(' '); 18 | } 19 | 20 | method description-comment(Str $indent = '') 21 | { 22 | return '' unless $!description; 23 | 24 | $indent ~ wrap-text($!description, :prefix("$indent# ")) ~ "\n"; 25 | } 26 | 27 | method Str { $!name } 28 | } 29 | 30 | # This is a placeholder for types not yet defined that will get replaced later. 31 | class GraphQL::LazyType is GraphQL::Type 32 | {} 33 | 34 | role Deprecatable 35 | { 36 | has Bool $.isDeprecated = False; 37 | has Str $.deprecationReason; 38 | 39 | method deprecate(Str $reason = "No longer supported.") 40 | { 41 | $!isDeprecated = True; 42 | $!deprecationReason = $reason; 43 | } 44 | 45 | method deprecate-str 46 | { 47 | ' @deprecated(reason: "' ~ $!deprecationReason ~ '")' 48 | if $!isDeprecated; 49 | } 50 | } 51 | 52 | class GraphQL::Scalar is GraphQL::Type 53 | { 54 | method kind(--> Str) { 'SCALAR' }; 55 | 56 | method Str { self.description-comment ~ "scalar $.name\n" } 57 | 58 | method to-json($name, $value, $indent) 59 | { 60 | qq<$indent"$name": > ~ to-json($value) 61 | } 62 | } 63 | 64 | class GraphQL::String is GraphQL::Scalar 65 | { 66 | has Str $.name = 'String'; 67 | has $.class = Str; 68 | 69 | multi method coerce(Any:U $value) { Str } 70 | multi method coerce(Any:D $value) { ~$value } 71 | } 72 | 73 | class GraphQL::Int is GraphQL::Scalar 74 | { 75 | has Str $.name = 'Int'; 76 | has $.class = Int; 77 | 78 | method coerce($value) { $value.Int } 79 | } 80 | 81 | class GraphQL::Float is GraphQL::Scalar 82 | { 83 | has Str $.name = 'Float'; 84 | has $.class = Num; 85 | 86 | method coerce($value) { $value.Num } 87 | } 88 | 89 | class GraphQL::Boolean is GraphQL::Scalar 90 | { 91 | has Str $.name = 'Boolean'; 92 | has $.class = Bool; 93 | 94 | method coerce($value) { $value } 95 | 96 | method to-json($name, $value, $indent) 97 | { 98 | qq<$indent"$name": > ~ ($value ?? 'true' !! 'false') 99 | } 100 | } 101 | 102 | class GraphQL::ID is GraphQL::Scalar 103 | { 104 | has Str $.name = 'ID'; 105 | has $.class = Cool; 106 | 107 | method coerce($value) { $value } 108 | } 109 | 110 | # 111 | # Default Types 112 | # 113 | 114 | sub GraphQLString is export 115 | { state $GraphQLString = GraphQL::String.new } 116 | 117 | sub GraphQLFloat is export 118 | { state $GraphQLFloat = GraphQL::Float.new } 119 | 120 | sub GraphQLInt is export 121 | { state $GraphQLInt = GraphQL::Int.new } 122 | 123 | sub GraphQLBoolean is export 124 | { state $GraphQLBoolean = GraphQL::Boolean.new } 125 | 126 | sub GraphQLID is export 127 | { state $GraphQLID = GraphQL::ID.new } 128 | 129 | class GraphQL::List is GraphQL::Type 130 | { 131 | has GraphQL::Type $.ofType is rw; 132 | 133 | method kind(--> Str) { 'LIST' }; 134 | 135 | method name { '[' ~ $.ofType.name ~ ']' } 136 | 137 | method to-json($name, $value, $indent) 138 | { 139 | qq<$indent"$name": \[\n> ~ 140 | $value.map({ $!ofType.to-json(Str, $_, $indent ~ ' ') }) 141 | .join(",\n") ~ 142 | qq<\n$indent]> 143 | } 144 | 145 | method coerce($value) 146 | { 147 | return Array[$!ofType.class] unless $value ~~ Array; 148 | 149 | Array[$!ofType.class].new($value.map({ $!ofType.coerce($_) })) 150 | } 151 | } 152 | 153 | class GraphQL::Non-Null is GraphQL::Type 154 | { 155 | has GraphQL::Type $.ofType is rw; 156 | 157 | method kind(--> Str) { 'NON_NULL' }; 158 | 159 | method name { $!ofType.name ~ '!' } 160 | 161 | method Str { $!ofType.Str ~ '!' } 162 | 163 | method coerce($value) 164 | { 165 | die "Null in Non-Null field" unless $value.defined; 166 | $!ofType.coerce($value) 167 | } 168 | 169 | method to-json($name, $value, $indent) 170 | { 171 | $!ofType.to-json($name, $value, $indent); 172 | } 173 | } 174 | 175 | class GraphQL::InputValue is GraphQL::Type 176 | { 177 | has GraphQL::Type $.type is rw; 178 | has $.defaultValue; 179 | 180 | method Str 181 | { 182 | "$.name: $.type.name()" ~ (" = $.defaultValue" 183 | if $.defaultValue.defined) 184 | } 185 | } 186 | 187 | class GraphQL::Field is GraphQL::Type does Deprecatable 188 | { 189 | has GraphQL::Type $.type is rw; 190 | has GraphQL::InputValue @.args is rw; 191 | has Callable $.resolver is rw; 192 | 193 | method Str(Str $indent = '') 194 | { 195 | self.description-comment($indent) ~ 196 | "$indent$.name" ~ 197 | ('(' ~ @!args.join(', ') ~ ')' if @!args) 198 | ~ ": $!type.name()" ~ self.deprecate-str 199 | } 200 | } 201 | 202 | role HasFields 203 | { 204 | has GraphQL::Field @.fieldlist; 205 | 206 | method field(Str $name) 207 | { 208 | @!fieldlist.first: *.name eq $name; 209 | } 210 | 211 | method fields(Bool :$includeDeprecated) 212 | { 213 | @!fieldlist.grep: {.name !~~ /^__/ and 214 | ($includeDeprecated or not .isDeprecated) } 215 | } 216 | 217 | method fields-str (Str $indent = '') 218 | { 219 | self.fields(:includeDeprecated).map({.Str($indent)}).join("\n") 220 | } 221 | } 222 | 223 | class GraphQL::Interface is GraphQL::Type does HasFields 224 | { 225 | has GraphQL::Type @.possibleTypes; 226 | 227 | method kind(--> Str) { 'INTERFACE' } 228 | 229 | method Str 230 | { 231 | self.description-comment ~ 232 | "interface $.name \{\n" ~ self.fields-str(' ') ~ "\n}\n" 233 | } 234 | } 235 | 236 | class GraphQL::Union is GraphQL::Type 237 | { 238 | has GraphQL::Type @.possibleTypes; 239 | 240 | method kind(--> Str) { 'UNION' } 241 | 242 | method Str 243 | { 244 | self.description-comment ~ 245 | "union $.name = { (@!possibleTypes».name).join(' | ') }\n"; 246 | } 247 | } 248 | 249 | class GraphQL::Object is GraphQL::Type does HasFields 250 | { 251 | has GraphQL::Type @.interfaces is rw; 252 | 253 | method kind(--> Str) { 'OBJECT' }; 254 | 255 | method addfield($field) { push @!fieldlist, $field } 256 | 257 | method fragment-applies(GraphQL::Type $fragmentType --> Bool) 258 | { 259 | given $fragmentType 260 | { 261 | when GraphQL::Object 262 | { 263 | $fragmentType === self; 264 | } 265 | 266 | when GraphQL::Interface | GraphQL::Union 267 | { 268 | .possibleTypes.first(self).defined 269 | } 270 | } 271 | } 272 | 273 | method Str 274 | { 275 | self.description-comment ~ 276 | "type $.name " ~ 277 | ('implements ' ~ (@!interfaces».name).join(', ') ~ ' ' 278 | if @.interfaces) 279 | ~ "\{\n" ~ self.fields-str(' ') ~ "\n}\n" 280 | } 281 | 282 | method to-json($name, $value, $indent) 283 | { 284 | $indent ~ (qq<"$name": > if $name) ~ 285 | 286 | ($value 287 | 288 | ?? "\{\n" ~ 289 | $value.map({ .to-json($indent ~ ' ') }).join(",\n") ~ 290 | qq<\n$indent}> 291 | 292 | !! 'null') 293 | } 294 | } 295 | 296 | class GraphQL::Input is GraphQL::Type 297 | { 298 | has GraphQL::InputValue @.inputFields; 299 | has $.class; 300 | 301 | method kind(--> Str) { 'INPUT_OBJECT' } 302 | 303 | method Str 304 | { 305 | self.description-comment ~ 306 | "input $.name " ~ 307 | ~ "\{\n" ~ @!inputFields.map({' ' ~ .Str}).join("\n") ~ "\n}\n" 308 | } 309 | 310 | multi method coerce(Any:U) 311 | { 312 | $.class; 313 | } 314 | 315 | multi method coerce(%value) 316 | { 317 | my %c; 318 | for @!inputFields -> $f 319 | { 320 | %c{$f.name} = $f.type.coerce(%value{$f.name}) 321 | if %value{$f.name}:exists; 322 | } 323 | 324 | return $!class ~~ GraphQL::InputObject 325 | ?? $!class.new(|%c) 326 | !! %c; 327 | } 328 | } 329 | 330 | class GraphQL::EnumValue is GraphQL::Scalar does Deprecatable 331 | { 332 | method Str(Str $indent = '') 333 | { self.description-comment ~ "$indent$.name" ~ self.deprecate-str } 334 | } 335 | 336 | class GraphQL::Enum is GraphQL::Type 337 | { 338 | has GraphQL::EnumValue @.enumValues; 339 | has $.enum; 340 | 341 | method kind(--> Str) { 'ENUM' } 342 | 343 | method enumValues(Bool :$includeDeprecated) 344 | { 345 | @!enumValues.grep: { $includeDeprecated or not .isDeprecated } 346 | } 347 | 348 | method valid($value) returns Bool 349 | { 350 | return Nil unless $value.defined; 351 | so @.enumValues.first({ .name eq $value }); 352 | } 353 | 354 | method coerce($value) 355 | { 356 | my \t := $.enum; 357 | 358 | $.enum ~~ Enumeration 359 | ?? t::{$value} 360 | !! (@.enumValues.first: {.name eq $value}).name 361 | } 362 | 363 | method Str 364 | { 365 | self.description-comment ~ 366 | "enum $.name \{\n" ~ 367 | @!enumValues.map({ $_.Str(' ')}).join("\n") ~ 368 | "\n}\n"; 369 | } 370 | 371 | method to-json($name, $value, $indent) 372 | { 373 | qq<$indent"$name": > ~ ($value.defined ?? qq<"$value"> !! 'null'); 374 | } 375 | } 376 | 377 | class GraphQL::Directive is GraphQL::Type 378 | { 379 | has GraphQL::EnumValue @.locations; 380 | has GraphQL::InputValue @.args; 381 | } 382 | 383 | class GraphQL::Variable 384 | { 385 | has Str $.name; 386 | has GraphQL::Type $.type; 387 | has $.defaultValue; 388 | 389 | method Str 390 | { 391 | "\$$!name: $!type.name()" ~ 392 | (" = $!defaultValue" if $!defaultValue.defined) 393 | } 394 | } 395 | 396 | class GraphQL::Operation 397 | { 398 | has Str $.name; 399 | has Str $.operation = 'query'; 400 | has GraphQL::Variable @.vars; 401 | has GraphQL::Directive @.directives; 402 | has @.selectionset; # QueryField or Fragment 403 | 404 | method Str 405 | { 406 | ("$.operation $.name " if $.name) ~ 407 | ( '(' ~ @.vars.map({.Str}).join(', ') ~ ') ' if @.vars) ~ "\{\n" ~ 408 | @.selectionset.map({.Str(' ')}).join('') ~ 409 | "}\n" 410 | } 411 | } 412 | 413 | sub directive-str($name, $args) 414 | { 415 | ' @' ~ $name ~ ('(' 416 | ~ $args.keys 417 | .map({ "$_: \$" ~ $args{$_}.name}) 418 | .join(', ') 419 | ~ ')' if $args) 420 | } 421 | 422 | sub argvalue($val) 423 | { 424 | given $val 425 | { 426 | when Hash 427 | { 428 | '{ ' ~ $val.keys.map({ "$_: " ~ argvalue($val{$_})}) ~ ' }' 429 | } 430 | when Array 431 | { 432 | ... 433 | } 434 | when GraphQL::Variable 435 | { 436 | "\$$val.name()"; 437 | } 438 | when Bool 439 | { 440 | $val.defined ?? ($val ?? 'true' !! 'false') 441 | !! 'null' 442 | } 443 | default 444 | { 445 | $val.perl 446 | } 447 | } 448 | } 449 | 450 | class GraphQL::QueryField 451 | { 452 | has Str $.alias; 453 | has Str $.name; 454 | has %.args; 455 | has %.directives; 456 | has @.selectionset; 457 | 458 | method responseKey { $!alias // $!name } 459 | 460 | method Str(Str $indent = '') 461 | { 462 | $indent ~ ($!alias ~ ': ' if $!alias) ~ $!name 463 | ~ 464 | ( '(' ~ %!args.keys.map({$_.Str ~ ': ' ~ argvalue(%!args{$_})}) 465 | .join(', ') ~ ')' if %!args) 466 | ~ 467 | %!directives.kv.map(&directive-str) 468 | ~ 469 | ( " \{\n" ~ @!selectionset.map({.Str($indent ~ ' ')}).join('') ~ 470 | $indent ~ '}' if @!selectionset) 471 | ~ "\n" 472 | } 473 | } 474 | 475 | class GraphQL::Fragment 476 | { 477 | has Str $.name; 478 | has GraphQL::Type $.onType; 479 | has %.directives; 480 | has @.selectionset; 481 | 482 | method Str($indent = '') 483 | { 484 | "fragment $.name on $.onType.name()" ~ 485 | ( " \{\n" ~ @!selectionset.map({.Str($indent ~ ' ')}).join('') ~ 486 | $indent ~ '}' if @!selectionset) 487 | } 488 | } 489 | 490 | class GraphQL::InlineFragment 491 | { 492 | has GraphQL::Type $.onType; 493 | has %.directives; 494 | has @.selectionset; 495 | 496 | method Str($indent = '') 497 | { 498 | "$indent..." 499 | ~ (" on $.onType" if $.onType) 500 | ~ " \{\n" ~ @!selectionset.map({.Str($indent ~ ' ')}).join('') 501 | ~ $indent ~ "}\n" 502 | } 503 | } 504 | 505 | class GraphQL::FragmentSpread 506 | { 507 | has Str $.name; 508 | has %.directives; 509 | 510 | method Str($indent = '') 511 | { 512 | "$indent... $.name\n" 513 | } 514 | } 515 | 516 | class GraphQL::Document 517 | { 518 | has GraphQL::Operation %.operations; 519 | has GraphQL::Fragment %.fragments; 520 | 521 | method GetOperation($operationName) 522 | { 523 | if $operationName 524 | { 525 | return %!operations{$operationName} 526 | if %!operations{$operationName}; 527 | 528 | die "Must provide an operation."; 529 | } 530 | 531 | return %!operations.values.first if %!operations.elems == 1; 532 | 533 | die "Must provide operation name if query contains multiple operations." 534 | } 535 | 536 | method Str 537 | { 538 | (%.operations.values.map({.Str}).join("\n"), 539 | %.fragments.values.map({.Str}).join("\n")).join("\n") 540 | ~ "\n"; 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /lib/GraphQL/Validation.pm: -------------------------------------------------------------------------------- 1 | unit module GraphQL::Validation; 2 | 3 | use GraphQL::Types; 4 | 5 | sub ValidateDocument(:$document, :$schema --> Bool) is export 6 | { 7 | for $document.operations.values -> $operation 8 | { 9 | validate-operation(:$document, :$schema, :$operation); 10 | } 11 | 12 | for $document.fragments.values -> $fragment 13 | { 14 | validate-selectionset(:$document, :$schema, 15 | type => $fragment.onType, 16 | selectionset => $fragment.selectionset); 17 | } 18 | return True; 19 | } 20 | 21 | sub validate-operation(:$document, :$schema, :$operation) 22 | { 23 | my $type = $operation.operation eq 'mutation' 24 | ?? $schema.mutationType 25 | !! $schema.queryType; 26 | 27 | my @selectionset = $operation.selectionset; 28 | 29 | validate-selectionset(:$document, :$schema, :$type, :@selectionset); 30 | } 31 | 32 | sub validate-selectionset(:$document, :$schema, :$type, :@selectionset) 33 | { 34 | if $type ~~ GraphQL::Union 35 | { 36 | for $type.possibleTypes -> $type 37 | { 38 | validate-selectionset(:$document, :$schema, :$type, :@selectionset); 39 | } 40 | return True; 41 | } 42 | 43 | for @selectionset 44 | { 45 | when GraphQL::InlineFragment 46 | { 47 | if $type.fragment-applies(.onType) 48 | { 49 | validate-selectionset(:$document, :$schema, :$type, 50 | selectionset => .selectionset); 51 | } 52 | } 53 | when GraphQL::FragmentSpread 54 | { 55 | my $fragment = $document.fragments{.name}; 56 | 57 | validate-selectionset(:$document, :$schema, :$type, 58 | selectionset => $fragment.selectionset); 59 | } 60 | when GraphQL::QueryField 61 | { 62 | die "Field $_.name() not defined" unless $type.field(.name); 63 | } 64 | } 65 | 66 | return True; 67 | } 68 | -------------------------------------------------------------------------------- /logotype/logo_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CurtTilmes/Perl6-GraphQL/b05d61c53f52502045609e79d8c01036a65e0ef7/logotype/logo_32x32.png -------------------------------------------------------------------------------- /t/01-parse-bad.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use Test; 4 | use lib 'lib'; 5 | 6 | use GraphQL::Grammar; 7 | 8 | my @query = 9 | # 1 10 | '{this', 11 | 'expected } at line 1', 12 | 13 | ; 14 | 15 | for @query -> $query, $expected 16 | { 17 | try 18 | { 19 | GraphQL::Grammar.parse($query, rule => 'Document') || die; 20 | CATCH 21 | { 22 | default 23 | { 24 | like ~$_, /$expected/, "Caught parse error"; 25 | } 26 | } 27 | } 28 | } 29 | 30 | done-testing; 31 | -------------------------------------------------------------------------------- /t/01-parse.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use Test; 4 | use lib 'lib'; 5 | 6 | use GraphQL::Grammar; 7 | 8 | # Every example from http://facebook.github.io/graphql 9 | 10 | my @good = Q<< 11 | { 12 | user(id: 4) { 13 | name 14 | } 15 | } 16 | >>, Q<< 17 | mutation { 18 | likeStory(storyID: 12345) { 19 | story { 20 | likeCount 21 | } 22 | } 23 | } 24 | >>, Q<< 25 | { 26 | field 27 | } 28 | >>, Q<< 29 | { 30 | id 31 | firstName 32 | lastName 33 | } 34 | >>, Q<< 35 | { 36 | me { 37 | id 38 | firstName 39 | lastName 40 | birthday { 41 | month 42 | day 43 | } 44 | friends { 45 | name 46 | } 47 | } 48 | } 49 | >>, Q<< 50 | # `me` could represent the currently logged in viewer. 51 | { 52 | me { 53 | name 54 | } 55 | } 56 | >>, Q<< 57 | # `user` represents one of many users in a graph of data, referred to by a 58 | # unique identifier. 59 | { 60 | user(id: 4) { 61 | name 62 | } 63 | } 64 | >>, Q<< 65 | { 66 | user(id: 4) { 67 | id 68 | name 69 | profilePic(size: 100) 70 | } 71 | } 72 | >>, Q<< 73 | { 74 | user(id: 4) { 75 | id 76 | name 77 | profilePic(width: 100, height: 50) 78 | } 79 | } 80 | >>, Q<< 81 | { 82 | user(id: 4) { 83 | id 84 | name 85 | smallPic: profilePic(size: 64) 86 | bigPic: profilePic(size: 1024) 87 | } 88 | } 89 | >>, Q<< 90 | { 91 | zuck: user(id: 4) { 92 | id 93 | name 94 | } 95 | } 96 | >>, Q<< 97 | query noFragments { 98 | user(id: 4) { 99 | friends(first: 10) { 100 | id 101 | name 102 | profilePic(size: 50) 103 | } 104 | mutualFriends(first: 10) { 105 | id 106 | name 107 | profilePic(size: 50) 108 | } 109 | } 110 | } 111 | >>, Q<< 112 | query withFragments { 113 | user(id: 4) { 114 | friends(first: 10) { 115 | ...friendFields 116 | } 117 | mutualFriends(first: 10) { 118 | ...friendFields 119 | } 120 | } 121 | } 122 | 123 | fragment friendFields on User { 124 | id 125 | name 126 | profilePic(size: 50) 127 | } 128 | >>, Q<< 129 | query withNestedFragments { 130 | user(id: 4) { 131 | friends(first: 10) { 132 | ...friendFields 133 | } 134 | mutualFriends(first: 10) { 135 | ...friendFields 136 | } 137 | } 138 | } 139 | 140 | fragment friendFields on User { 141 | id 142 | name 143 | ...standardProfilePic 144 | } 145 | 146 | fragment standardProfilePic on User { 147 | profilePic(size: 50) 148 | } 149 | >>, Q<< 150 | query FragmentTyping { 151 | profiles(handles: ["zuck", "cocacola"]) { 152 | handle 153 | ...userFragment 154 | ...pageFragment 155 | } 156 | } 157 | 158 | fragment userFragment on User { 159 | friends { 160 | count 161 | } 162 | } 163 | 164 | fragment pageFragment on Page { 165 | likers { 166 | count 167 | } 168 | } 169 | >>, Q<< 170 | query inlineFragmentTyping { 171 | profiles(handles: ["zuck", "cocacola"]) { 172 | handle 173 | ... on User { 174 | friends { 175 | count 176 | } 177 | } 178 | ... on Page { 179 | likers { 180 | count 181 | } 182 | } 183 | } 184 | } 185 | >>,Q<< 186 | query inlineFragmentNoType($expandedInfo: Boolean) { 187 | user(handle: "zuck") { 188 | id 189 | name 190 | ... @include(if: $expandedInfo) { 191 | firstName 192 | lastName 193 | birthday 194 | } 195 | } 196 | } 197 | >>,Q<< 198 | { 199 | field(arg: null) 200 | field 201 | } 202 | >>,Q<< 203 | { 204 | nearestThing(location: { lon: 12.43, lat: -53.211 }) 205 | } 206 | { 207 | nearestThing(location: { lat: -53.211, lon: 12.43 }) 208 | } 209 | >>,Q<< 210 | query getZuckProfile($devicePicSize: Int) { 211 | user(id: 4) { 212 | id 213 | name 214 | profilePic(size: $devicePicSize) 215 | } 216 | } 217 | >>,Q<< 218 | { 219 | name 220 | age 221 | picture 222 | } 223 | >>,Q<< 224 | { 225 | age 226 | name 227 | } 228 | >>,Q<< 229 | { 230 | name 231 | relationship { 232 | name 233 | } 234 | } 235 | >>,Q<< 236 | { 237 | foo 238 | ...Frag 239 | qux 240 | } 241 | 242 | fragment Frag on Query { 243 | bar 244 | baz 245 | } 246 | >>,Q<< 247 | { 248 | foo 249 | ...Ignored 250 | ...Matching 251 | bar 252 | } 253 | 254 | fragment Ignored on UnknownType { 255 | qux 256 | baz 257 | } 258 | 259 | fragment Matching on Query { 260 | bar 261 | qux 262 | foo 263 | } 264 | >>,Q<< 265 | { 266 | foo @skip(if: true) 267 | bar 268 | foo 269 | } 270 | >>,Q<< 271 | { 272 | name 273 | picture(size: 600) 274 | } 275 | >>,Q<< 276 | { 277 | entity { 278 | name 279 | } 280 | phoneNumber 281 | } 282 | >>,Q<< 283 | query myQuery($someTest: Boolean) { 284 | experimentalField @skip(if: $someTest) 285 | } 286 | >>,Q<< 287 | query myQuery($someTest: Boolean) { 288 | experimentalField @include(if: $someTest) 289 | } 290 | >>,Q<< 291 | query getMe { 292 | me 293 | } 294 | >>,Q<< 295 | mutation setName { 296 | setName(name: "Zuck") { 297 | newName 298 | } 299 | } 300 | >>,Q<< 301 | { 302 | __type(name: "User") { 303 | name 304 | fields { 305 | name 306 | type { 307 | name 308 | } 309 | } 310 | } 311 | } 312 | >>,Q<< 313 | query getDogName { 314 | dog { 315 | name 316 | } 317 | } 318 | 319 | query getOwnerName { 320 | dog { 321 | owner { 322 | name 323 | } 324 | } 325 | } 326 | >>,Q<< 327 | { 328 | dog { 329 | name 330 | } 331 | } 332 | >>,Q<< 333 | { 334 | dog { 335 | ...fragmentOne 336 | ...fragmentTwo 337 | } 338 | } 339 | 340 | fragment fragmentOne on Dog { 341 | name 342 | } 343 | 344 | fragment fragmentTwo on Dog { 345 | owner { 346 | name 347 | } 348 | } 349 | >>,Q<< 350 | query houseTrainedQuery($atOtherHomes: Boolean = true) { 351 | dog { 352 | isHousetrained(atOtherHomes: $atOtherHomes) 353 | } 354 | } 355 | >>,Q<< 356 | query houseTrainedQuery($atOtherHomes: Boolean! = true) { 357 | dog { 358 | isHousetrained(atOtherHomes: $atOtherHomes) 359 | } 360 | } 361 | >>,Q<< 362 | query houseTrainedQuery($atOtherHomes: Boolean = "true") { 363 | dog { 364 | isHousetrained(atOtherHomes: $atOtherHomes) 365 | } 366 | } 367 | >>; 368 | 369 | for @good -> $query 370 | { 371 | ok GraphQL::Grammar.parse($query, rule => 'Document'); 372 | } 373 | 374 | done-testing; 375 | -------------------------------------------------------------------------------- /t/02-schemaparse.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use Test; 4 | use lib 'lib'; 5 | 6 | use GraphQL; 7 | use GraphQL::Types; 8 | 9 | my @testcases = 10 | 'Simple Hello with a string', 11 | Q<< 12 | type Query { 13 | hello: String 14 | } 15 | >>, 16 | GraphQL::Schema.new( 17 | GraphQL::Object.new( 18 | name => 'Query', 19 | fields => GraphQL::Field.new( 20 | name => 'hello', 21 | type => GraphQLString 22 | ) 23 | ) 24 | ), 25 | #---------------------------------------------------------------------- 26 | 'Comments for description field', 27 | Q<< 28 | # Query description 29 | type Query { 30 | # field description for hello 31 | hello: String 32 | } 33 | >>, 34 | GraphQL::Schema.new( 35 | GraphQL::Object.new( 36 | name => 'Query', 37 | description => 'Query description', 38 | fields => GraphQL::Field.new( 39 | name => 'hello', 40 | description => 'field description for hello', 41 | type => GraphQLString 42 | ) 43 | ) 44 | ), 45 | #---------------------------------------------------------------------- 46 | 'Non-null', 47 | Q<< 48 | type Query { 49 | hello: String! 50 | } 51 | >>, 52 | GraphQL::Schema.new( 53 | GraphQL::Object.new( 54 | name => 'Query', 55 | fields => GraphQL::Field.new( 56 | name => 'hello', 57 | type => GraphQL::Non-Null.new(ofType => GraphQLString) 58 | ) 59 | ) 60 | ), 61 | #---------------------------------------------------------------------- 62 | 'List of String', 63 | Q<< 64 | type Query { 65 | hello: [String] 66 | } 67 | >>, 68 | GraphQL::Schema.new( 69 | GraphQL::Object.new( 70 | name => 'Query', 71 | fields => GraphQL::Field.new( 72 | name => 'hello', 73 | type => GraphQL::List.new(ofType => GraphQLString) 74 | ) 75 | ) 76 | ), 77 | #---------------------------------------------------------------------- 78 | 'Non-null List of Non-null String', 79 | Q<< 80 | type Query { 81 | hello: [String!]! 82 | } 83 | >>, 84 | GraphQL::Schema.new( 85 | GraphQL::Object.new( 86 | name => 'Query', 87 | fields => GraphQL::Field.new( 88 | name => 'hello', 89 | type => GraphQL::Non-Null.new( 90 | ofType => GraphQL::List.new( 91 | ofType => GraphQL::Non-Null.new( 92 | ofType => GraphQLString) 93 | ) 94 | ) 95 | ) 96 | ) 97 | ), 98 | #---------------------------------------------------------------------- 99 | 'Arguments with various scalar types', 100 | Q<< 101 | type Query { 102 | id: ID! 103 | name: String 104 | age: Int 105 | balance: Float 106 | is_active: Boolean 107 | } 108 | >>, 109 | GraphQL::Schema.new( 110 | GraphQL::Object.new( 111 | name => 'Query', 112 | fields => ( 113 | GraphQL::Field.new( 114 | name => 'id', 115 | type => GraphQL::Non-Null.new( 116 | ofType => GraphQLID) 117 | ), 118 | GraphQL::Field.new( 119 | name => 'name', 120 | type => GraphQLString 121 | ), 122 | GraphQL::Field.new( 123 | name => 'age', 124 | type => GraphQLInt 125 | ), 126 | GraphQL::Field.new( 127 | name => 'balance', 128 | type => GraphQLFloat 129 | ), 130 | GraphQL::Field.new( 131 | name => 'is_active', 132 | type => GraphQLBoolean 133 | ) 134 | ) 135 | ) 136 | ), 137 | #---------------------------------------------------------------------- 138 | 'Field with argument', 139 | Q<< 140 | type Query { 141 | hello(limit: Int): String 142 | } 143 | >>, 144 | GraphQL::Schema.new( 145 | GraphQL::Object.new( 146 | name => 'Query', 147 | fields => ( 148 | GraphQL::Field.new( 149 | name => 'hello', 150 | type => GraphQLString, 151 | args => 152 | [ 153 | GraphQL::InputValue.new( 154 | name => 'limit', 155 | type => GraphQLInt 156 | ) 157 | ] 158 | ) 159 | ) 160 | ) 161 | ), 162 | #---------------------------------------------------------------------- 163 | 'Field with argument with default value', 164 | Q<< 165 | type Query { 166 | hello(limit: Int = 10): String 167 | } 168 | >>, 169 | GraphQL::Schema.new( 170 | GraphQL::Object.new( 171 | name => 'Query', 172 | fields => ( 173 | GraphQL::Field.new( 174 | name => 'hello', 175 | type => GraphQLString, 176 | args => 177 | [ 178 | GraphQL::InputValue.new( 179 | name => 'limit', 180 | type => GraphQLInt, 181 | defaultValue => 10 182 | ) 183 | ] 184 | ) 185 | ) 186 | ) 187 | ), 188 | #---------------------------------------------------------------------- 189 | 'Field with arguments of various types', 190 | Q<< 191 | type Query { 192 | hello(id: ID, first: Int, x: Float, cond: Boolean, person: String): String 193 | } 194 | >>, 195 | GraphQL::Schema.new( 196 | GraphQL::Object.new( 197 | name => 'Query', 198 | fields => ( 199 | GraphQL::Field.new( 200 | name => 'hello', 201 | type => GraphQLString, 202 | args => 203 | [ 204 | GraphQL::InputValue.new( 205 | name => 'id', 206 | type => GraphQLID 207 | ), 208 | GraphQL::InputValue.new( 209 | name => 'first', 210 | type => GraphQLInt 211 | ), 212 | GraphQL::InputValue.new( 213 | name => 'x', 214 | type => GraphQLFloat 215 | ), 216 | GraphQL::InputValue.new( 217 | name => 'cond', 218 | type => GraphQLBoolean 219 | ), 220 | GraphQL::InputValue.new( 221 | name => 'person', 222 | type => GraphQLString 223 | ) 224 | ] 225 | ) 226 | ) 227 | ) 228 | ), 229 | #---------------------------------------------------------------------- 230 | 'Field with arguments of various types with defaults', 231 | Q<< 232 | type Query { 233 | hello(id: ID = "123xyz", 234 | first: Int = 27, 235 | x: Float = 1.2, 236 | cond: Boolean = true, 237 | person: String = "Fred"): String 238 | } 239 | >>, 240 | GraphQL::Schema.new( 241 | GraphQL::Object.new( 242 | name => 'Query', 243 | fields => 244 | GraphQL::Field.new( 245 | name => 'hello', 246 | type => GraphQLString, 247 | args => 248 | [ 249 | GraphQL::InputValue.new( 250 | name => 'id', 251 | type => GraphQLID, 252 | defaultValue => '123xyz' 253 | ), 254 | GraphQL::InputValue.new( 255 | name => 'first', 256 | type => GraphQLInt, 257 | defaultValue => 27 258 | ), 259 | GraphQL::InputValue.new( 260 | name => 'x', 261 | type => GraphQLFloat, 262 | defaultValue => Num(1.2) 263 | ), 264 | GraphQL::InputValue.new( 265 | name => 'cond', 266 | type => GraphQLBoolean, 267 | defaultValue => True, 268 | ), 269 | GraphQL::InputValue.new( 270 | name => 'person', 271 | type => GraphQLString, 272 | defaultValue => 'Fred' 273 | ) 274 | ] 275 | ) 276 | ) 277 | ), 278 | #---------------------------------------------------------------------- 279 | ; 280 | 281 | for @testcases -> $description, $query, $schema 282 | { 283 | ok my $testschema = GraphQL::Schema.new($query), "Parsing $description"; 284 | 285 | is-deeply($testschema, $schema, $description); 286 | } 287 | 288 | done-testing; 289 | -------------------------------------------------------------------------------- /t/03-schemafull.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | 3 | use GraphQL; 4 | use GraphQL::Types; 5 | use GraphQL::Compare; 6 | 7 | my $schemastring = 8 | Q<< 9 | # Entity interface 10 | interface Entity { 11 | # id field 12 | id: ID! 13 | # name field 14 | name: String 15 | } 16 | 17 | # Foo interface 18 | interface Foo { 19 | is_foo: Boolean 20 | } 21 | 22 | # Goo interface 23 | interface Goo { 24 | is_goo: Boolean 25 | } 26 | 27 | # Bar object type 28 | type Bar implements Foo { 29 | is_foo: Boolean 30 | is_bar: Boolean 31 | } 32 | 33 | # Baz object type 34 | type Baz implements Foo, Goo { 35 | is_foo: Boolean 36 | is_goo: Boolean 37 | is_baz: Boolean 38 | } 39 | 40 | type Person { 41 | name: String 42 | } 43 | 44 | type Pet { 45 | name: String 46 | } 47 | 48 | # Single union of type person 49 | union SingleUnion = Person 50 | 51 | # Union can be either a Person or a Pet 52 | union MultipleUnion = Person | Pet 53 | 54 | # A Friend has a person (single), and either a Person or Pet (multiple) 55 | type Friend { 56 | single: SingleUnion 57 | multiple: MultipleUnion 58 | } 59 | 60 | # URL is a special type of scalar 61 | scalar URL 62 | 63 | # User is a type of Entity 64 | type User implements Entity { 65 | id: ID! 66 | name: String 67 | # website field is of special scalar type URL 68 | website: URL 69 | } 70 | 71 | # Root object type 72 | type Root { 73 | me: User 74 | } 75 | 76 | schema { 77 | query: Root 78 | } 79 | >>; 80 | 81 | ok my $Entity = GraphQL::Interface.new( 82 | name => 'Entity', 83 | description => 'Entity interface', 84 | fieldlist => ( 85 | GraphQL::Field.new( 86 | name => 'id', 87 | description => 'id field', 88 | type => GraphQL::Non-Null.new( 89 | ofType => GraphQLID 90 | ) 91 | ), 92 | GraphQL::Field.new( 93 | name => 'name', 94 | description => 'name field', 95 | type => GraphQLString 96 | ) 97 | ) 98 | ), 'Make Interface Entity'; 99 | 100 | ok my $Foo = GraphQL::Interface.new( 101 | name => 'Foo', 102 | description => 'Foo interface', 103 | fieldlist => ( 104 | GraphQL::Field.new( 105 | name => 'is_foo', 106 | type => GraphQLBoolean 107 | ) 108 | ) 109 | ), 'Make Interface Foo'; 110 | 111 | ok my $Goo = GraphQL::Interface.new( 112 | name => 'Goo', 113 | description => 'Goo interface', 114 | fieldlist => ( 115 | GraphQL::Field.new( 116 | name => 'is_goo', 117 | type => GraphQLBoolean 118 | ) 119 | ) 120 | ), 'Make Interface Goo'; 121 | 122 | ok my $Bar = GraphQL::Object.new( 123 | name => 'Bar', 124 | description => 'Bar object type', 125 | interfaces => [ $Foo ], 126 | fieldlist => ( 127 | GraphQL::Field.new( 128 | name => 'is_foo', 129 | type => GraphQLBoolean 130 | ), 131 | GraphQL::Field.new( 132 | name => 'is_bar', 133 | type => GraphQLBoolean 134 | ) 135 | ) 136 | ), 'Make Object Bar'; 137 | 138 | ok my $Baz = GraphQL::Object.new( 139 | name => 'Baz', 140 | description => 'Baz object type', 141 | interfaces => [ $Foo, $Goo ], 142 | fieldlist => ( 143 | GraphQL::Field.new( 144 | name => 'is_foo', 145 | type => GraphQLBoolean 146 | ), 147 | GraphQL::Field.new( 148 | name => 'is_goo', 149 | type => GraphQLBoolean 150 | ), 151 | GraphQL::Field.new( 152 | name => 'is_baz', 153 | type => GraphQLBoolean 154 | ) 155 | ) 156 | ), 'Make Object Baz'; 157 | 158 | ok my $Person = GraphQL::Object.new( 159 | name => 'Person', 160 | fieldlist => ( 161 | GraphQL::Field.new( 162 | name => 'name', 163 | type => GraphQLString 164 | ) 165 | ) 166 | ), 'Make Object Person'; 167 | 168 | ok my $Pet = GraphQL::Object.new( 169 | name => 'Pet', 170 | fieldlist => ( 171 | GraphQL::Field.new( 172 | name => 'name', 173 | type => GraphQLString 174 | ) 175 | ) 176 | ), 'Make Object Pet'; 177 | 178 | ok my $SingleUnion = GraphQL::Union.new( 179 | name => 'SingleUnion', 180 | description => 'Single union of type person', 181 | possibleTypes => $Person 182 | ), 'Make Union SingleUnion'; 183 | 184 | ok my $MultipleUnion = GraphQL::Union.new( 185 | name => 'MultipleUnion', 186 | description => 'Union can be either a Person or a Pet', 187 | possibleTypes => ($Person, $Pet) 188 | ), 'Make Union MultipleUnion'; 189 | 190 | ok my $Friend = GraphQL::Object.new( 191 | name => 'Friend', 192 | description => 'A Friend has a person (single), and either a Person or Pet (multiple)', 193 | fieldlist => ( 194 | GraphQL::Field.new( 195 | name => 'single', 196 | type => $SingleUnion 197 | ), 198 | GraphQL::Field.new( 199 | name => 'multiple', 200 | type => $MultipleUnion 201 | ) 202 | ) 203 | ), 'Make Object Friend'; 204 | 205 | ok my $URL = GraphQL::Scalar.new( 206 | name => 'URL', 207 | description => 'URL is a special type of scalar', 208 | ), 'Make named scalar'; 209 | 210 | ok my $User = GraphQL::Object.new( 211 | name => 'User', 212 | description => 'User is a type of Entity', 213 | interfaces => [ $Entity ], 214 | fieldlist => ( 215 | GraphQL::Field.new( 216 | name => 'id', 217 | type => GraphQL::Non-Null.new( 218 | ofType => GraphQLID 219 | ) 220 | ), 221 | GraphQL::Field.new( 222 | name => 'name', 223 | type => GraphQLString 224 | ), 225 | GraphQL::Field.new( 226 | name => 'website', 227 | description => 'website field is of special scalar type URL', 228 | type => $URL 229 | ) 230 | ) 231 | ), 'Make Object User'; 232 | 233 | ok my $schema = GraphQL::Schema.new( 234 | query => 'Root', 235 | $Entity, 236 | $Foo, 237 | $Goo, 238 | $Bar, 239 | $Baz, 240 | $Person, 241 | $Pet, 242 | $SingleUnion, 243 | $MultipleUnion, 244 | $Friend, 245 | $URL, 246 | $User, 247 | GraphQL::Object.new( 248 | name => 'Root', 249 | description => 'Root object type', 250 | fieldlist => ( 251 | GraphQL::Field.new( 252 | name => 'me', 253 | type => $User 254 | ) 255 | ) 256 | ) 257 | ), 'Make Schema'; 258 | 259 | $schema.resolve-schema; 260 | 261 | ok my $testschema = GraphQL::Schema.new($schemastring), 'Parse schema'; 262 | 263 | $testschema.resolve-schema; 264 | 265 | is-deeply $testschema.type('Entity'), $Entity, 'Compare Interface Entity'; 266 | 267 | # Not sure what changed, but these fail -- will look into at some point.. 268 | 269 | #is-deeply $testschema.type('Foo'), $Foo, 'Compare Interface Foo'; 270 | 271 | #is-deeply $testschema.type('Goo'), $Goo, 'Compare Interface Goo'; 272 | 273 | is-deeply $testschema.type('Bar'), $Bar, 'Compare Object Bar'; 274 | 275 | is-deeply $testschema.type('Baz'), $Baz, 'Compare Object Baz'; 276 | 277 | is-deeply $testschema.type('Person'), $Person, 'Compare Object Person'; 278 | 279 | is-deeply $testschema.type('Pet'), $Pet, 'Compare Object Pet'; 280 | 281 | is-deeply $testschema.type('SingleUnion'), $SingleUnion, 'Compare Union Single'; 282 | 283 | is-deeply $testschema.type('MultipleUnion'), $MultipleUnion, 284 | 'Compare Union Multiple'; 285 | 286 | is-deeply $testschema.type('Friend'), $Friend, 'Compare Object Friend'; 287 | 288 | is-deeply $testschema.type('URL'), $URL, 'Compare Scalar URL'; 289 | 290 | is-deeply $testschema.type('User'), $User, 'Compare Object User'; 291 | 292 | is-deeply $testschema, $schema, 'Compare whole schema'; 293 | 294 | done-testing; 295 | 296 | -------------------------------------------------------------------------------- /t/04-query.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use lib 'lib'; 4 | use GraphQL; 5 | use JSON::Fast; 6 | 7 | use Test; 8 | 9 | my $schema = GraphQL::Schema.new(' 10 | type User { 11 | id: String 12 | name: String 13 | birthday: String 14 | } 15 | 16 | schema { 17 | query: User 18 | } 19 | '); 20 | 21 | $schema.resolvers( 22 | { 23 | User => { 24 | id => sub { return 7 }, 25 | name => sub { return 'Fred' }, 26 | birthday => sub { return 'Friday' } 27 | } 28 | }); 29 | 30 | my @testcases = 31 | 'Single field', 32 | Q<< 33 | { 34 | name 35 | } 36 | >>, 37 | Q<<{ 38 | "data": { 39 | "name": "Fred" 40 | } 41 | }>>, 42 | #---------------------------------------------------------------------- 43 | 'More fields', 44 | Q<< 45 | { 46 | name 47 | id 48 | birthday 49 | } 50 | >>, 51 | Q<<{ 52 | "data": { 53 | "name": "Fred", 54 | "id": 7, 55 | "birthday": "Friday" 56 | } 57 | }>>, 58 | #---------------------------------------------------------------------- 59 | 'Try some aliases', 60 | Q<< 61 | { 62 | callme: name 63 | id 64 | mybday: birthday 65 | orcallme: name 66 | } 67 | >>, 68 | Q<<{ 69 | "data": { 70 | "callme": "Fred", 71 | "id": 7, 72 | "mybday": "Friday", 73 | "orcallme": "Fred" 74 | } 75 | }>>, 76 | #---------------------------------------------------------------------- 77 | 'Fragment', 78 | Q<< 79 | query { 80 | name 81 | id 82 | ... morestuff 83 | birthday 84 | } 85 | fragment morestuff on User { 86 | callme: name 87 | mybday: birthday 88 | } 89 | >>, 90 | Q<<{ 91 | "data": { 92 | "name": "Fred", 93 | "id": 7, 94 | "callme": "Fred", 95 | "mybday": "Friday", 96 | "birthday": "Friday" 97 | } 98 | }>>, 99 | #---------------------------------------------------------------------- 100 | 'inline Fragment', 101 | Q<< 102 | query foo { 103 | name 104 | id 105 | ... { 106 | callme: name 107 | mybday: birthday 108 | } 109 | birthday 110 | } 111 | >>, 112 | Q<<{ 113 | "data": { 114 | "name": "Fred", 115 | "id": 7, 116 | "callme": "Fred", 117 | "mybday": "Friday", 118 | "birthday": "Friday" 119 | } 120 | }>>, 121 | #---------------------------------------------------------------------- 122 | 'Introspection __type', 123 | Q<< 124 | { 125 | __type(name: "User") { 126 | name 127 | kind 128 | description 129 | } 130 | } 131 | >>, 132 | Q<<{ 133 | "data": { 134 | "__type": { 135 | "name": "User", 136 | "kind": "OBJECT", 137 | "description": null 138 | } 139 | } 140 | }>>, 141 | #---------------------------------------------------------------------- 142 | 'Introspection __type(Int)', 143 | Q<< 144 | { 145 | __type(name: "Int") { 146 | name 147 | kind 148 | description 149 | } 150 | } 151 | >>, 152 | Q<<{ 153 | "data": { 154 | "__type": { 155 | "name": "Int", 156 | "kind": "SCALAR", 157 | "description": null 158 | } 159 | } 160 | }>>, 161 | #---------------------------------------------------------------------- 162 | 'Introspection __type(String)', 163 | Q<< 164 | { 165 | __type(name: "String") { 166 | name 167 | kind 168 | description 169 | } 170 | } 171 | >>, 172 | Q<<{ 173 | "data": { 174 | "__type": { 175 | "name": "String", 176 | "kind": "SCALAR", 177 | "description": null 178 | } 179 | } 180 | }>>, 181 | #---------------------------------------------------------------------- 182 | 'Introspection __type(Boolean)', 183 | Q<< 184 | { 185 | __type(name: "Boolean") { 186 | name 187 | kind 188 | description 189 | } 190 | } 191 | >>, 192 | Q<<{ 193 | "data": { 194 | "__type": { 195 | "name": "Boolean", 196 | "kind": "SCALAR", 197 | "description": null 198 | } 199 | } 200 | }>>, 201 | #---------------------------------------------------------------------- 202 | 'Introspection __type(Float)', 203 | Q<< 204 | { 205 | __type(name: "Float") { 206 | name 207 | kind 208 | description 209 | } 210 | } 211 | >>, 212 | Q<<{ 213 | "data": { 214 | "__type": { 215 | "name": "Float", 216 | "kind": "SCALAR", 217 | "description": null 218 | } 219 | } 220 | }>>, 221 | 222 | ; 223 | 224 | for @testcases -> $description, $query, $expected 225 | { 226 | is $schema.execute($query).to-json, $expected, $description; 227 | } 228 | 229 | done-testing; 230 | -------------------------------------------------------------------------------- /t/05-objectresolver.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use lib 'lib'; 4 | use GraphQL; 5 | 6 | use Test; 7 | 8 | ok my $schema = GraphQL::Schema.new(' 9 | type User { 10 | id: ID 11 | name: String 12 | birthday: String 13 | status: Boolean 14 | someextra: String 15 | } 16 | 17 | type Query { 18 | user: User 19 | } 20 | 21 | schema { 22 | query: Query 23 | }'), 'Make schema'; 24 | 25 | class User 26 | { 27 | has $.id; 28 | has $.name; 29 | has $.birthday; 30 | has $.status; 31 | } 32 | 33 | my $somebody = User.new(id => 7, 34 | name => 'Fred', 35 | birthday => 'Friday', 36 | status => True); 37 | 38 | $schema.resolvers( 39 | { 40 | Query => { user => sub { return $somebody } } 41 | }); 42 | 43 | $schema.resolvers( 44 | { 45 | User => { 46 | someextra => sub { return "an extra field" } 47 | } 48 | }); 49 | 50 | ok my $document = $schema.document(' 51 | query { 52 | user { 53 | name 54 | id 55 | birthday 56 | status 57 | someextra 58 | } 59 | } 60 | '), 'Make document'; 61 | 62 | ok my $ret = $schema.execute(:$document), 'Execute query'; 63 | 64 | is $ret.to-json, 65 | Q<<{ 66 | "data": { 67 | "user": { 68 | "name": "Fred", 69 | "id": 7, 70 | "birthday": "Friday", 71 | "status": true, 72 | "someextra": "an extra field" 73 | } 74 | } 75 | }>>, 'Compare results'; 76 | 77 | done-testing; 78 | -------------------------------------------------------------------------------- /t/06-queries-with-args.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use lib 'lib'; 4 | use GraphQL; 5 | 6 | use Test; 7 | 8 | ok my $schema = GraphQL::Schema.new(' 9 | type User { 10 | id: ID 11 | name: String 12 | birthday: String 13 | status: Boolean 14 | } 15 | 16 | type Root { 17 | allusers(start: ID = 0, count: ID = 1): [User] 18 | user(id: ID): User 19 | } 20 | 21 | schema { 22 | query: Root 23 | } 24 | '), 'Build Schema'; 25 | 26 | class User 27 | { 28 | has $.id; 29 | has $.name; 30 | has $.birthday; 31 | has $.status; 32 | } 33 | 34 | my @users = 35 | User.new(id => 0, 36 | name => 'Gilligan', 37 | birthday => 'Friday', 38 | status => True), 39 | User.new(id => 1, 40 | name => 'Skipper', 41 | birthday => 'Monday', 42 | status => False), 43 | User.new(id => 2, 44 | name => 'Professor', 45 | birthday => 'Tuesday', 46 | status => True), 47 | User.new(id => 3, 48 | name => 'Ginger', 49 | birthday => 'Wednesday', 50 | status => True), 51 | User.new(id => 4, 52 | name => 'Mary Anne', 53 | birthday => 'Thursday', 54 | status => True); 55 | 56 | $schema.resolvers( 57 | { 58 | Root => 59 | { 60 | allusers => 61 | sub (Int :$start, Int :$count) 62 | { 63 | @users[$start ..^ $start+$count] 64 | }, 65 | user => 66 | sub (Int :$id) 67 | { 68 | @users[$id] 69 | } 70 | } 71 | }); 72 | 73 | my @testcases = 74 | 'Query for user 3', 75 | 76 | '{ user(id: 3) { id, name } }', 77 | 78 | {}, 79 | 80 | Q<<{ 81 | "data": { 82 | "user": { 83 | "id": 3, 84 | "name": "Ginger" 85 | } 86 | } 87 | }>>, 88 | 89 | #---------------------------------------------------------------------- 90 | 'Query for first user in allusers', 91 | 92 | '{ allusers { id name } }', 93 | 94 | {}, 95 | 96 | Q<<{ 97 | "data": { 98 | "allusers": [ 99 | { 100 | "id": 0, 101 | "name": "Gilligan" 102 | } 103 | ] 104 | } 105 | }>>, 106 | 107 | #---------------------------------------------------------------------- 108 | 'Query for 2 users starting with user 3', 109 | 110 | '{ allusers(start: 3, count: 2) { name status } }', 111 | 112 | {}, 113 | 114 | Q<<{ 115 | "data": { 116 | "allusers": [ 117 | { 118 | "name": "Ginger", 119 | "status": true 120 | }, 121 | { 122 | "name": "Mary Anne", 123 | "status": true 124 | } 125 | ] 126 | } 127 | }>>, 128 | 129 | #---------------------------------------------------------------------- 130 | 'Query for single user with variable', 131 | 132 | 'query ($x: ID) { user(id: $x) { id, name } }', 133 | 134 | { x => 3 }, 135 | 136 | Q<<{ 137 | "data": { 138 | "user": { 139 | "id": 3, 140 | "name": "Ginger" 141 | } 142 | } 143 | }>>, 144 | 145 | #---------------------------------------------------------------------- 146 | 'Query for another user with variable', 147 | 148 | 'query ($x: ID) { user(id: $x) { id, name } }', 149 | 150 | { x => 4 }, 151 | 152 | Q<<{ 153 | "data": { 154 | "user": { 155 | "id": 4, 156 | "name": "Mary Anne" 157 | } 158 | } 159 | }>>, 160 | 161 | #---------------------------------------------------------------------- 162 | 'Query for multiple users with multiple variables', 163 | 164 | 'query ($start: Int, $count: Int) 165 | { allusers(start: $start, count: $count) { name status } }', 166 | 167 | { start => 1, count => 4 }, 168 | 169 | Q<<{ 170 | "data": { 171 | "allusers": [ 172 | { 173 | "name": "Skipper", 174 | "status": false 175 | }, 176 | { 177 | "name": "Professor", 178 | "status": true 179 | }, 180 | { 181 | "name": "Ginger", 182 | "status": true 183 | }, 184 | { 185 | "name": "Mary Anne", 186 | "status": true 187 | } 188 | ] 189 | } 190 | }>>, 191 | 192 | #---------------------------------------------------------------------- 193 | '@skip directive if false', 194 | 195 | 'query { user(id: 4) { id, name @skip(if: false)} }', 196 | 197 | {}, 198 | 199 | Q<<{ 200 | "data": { 201 | "user": { 202 | "id": 4, 203 | "name": "Mary Anne" 204 | } 205 | } 206 | }>>, 207 | 208 | #---------------------------------------------------------------------- 209 | '@skip directive if true', 210 | 211 | 'query { user(id: 4) { id, name @skip(if: true)} }', 212 | 213 | {}, 214 | 215 | Q<<{ 216 | "data": { 217 | "user": { 218 | "id": 4 219 | } 220 | } 221 | }>>, 222 | 223 | #---------------------------------------------------------------------- 224 | '@skip directive if variable false', 225 | 226 | 'query ($x: Boolean) { user(id: 4) { id, name @skip(if: $x)} }', 227 | 228 | { x => False }, 229 | 230 | Q<<{ 231 | "data": { 232 | "user": { 233 | "id": 4, 234 | "name": "Mary Anne" 235 | } 236 | } 237 | }>>, 238 | 239 | #---------------------------------------------------------------------- 240 | '@include directive if variable true', 241 | 242 | 'query ($x: Boolean) { user(id: 4) { id, name @include(if: $x)} }', 243 | 244 | {x => True}, 245 | 246 | Q<<{ 247 | "data": { 248 | "user": { 249 | "id": 4, 250 | "name": "Mary Anne" 251 | } 252 | } 253 | }>>, 254 | 255 | #---------------------------------------------------------------------- 256 | '@include directive if false', 257 | 258 | 'query { user(id: 4) { id, name @include(if: false)} }', 259 | 260 | {}, 261 | 262 | Q<<{ 263 | "data": { 264 | "user": { 265 | "id": 4 266 | } 267 | } 268 | }>>, 269 | 270 | #---------------------------------------------------------------------- 271 | '@include directive if true', 272 | 273 | 'query { user(id: 4) { id, name @include(if: true)} }', 274 | 275 | {}, 276 | 277 | Q<<{ 278 | "data": { 279 | "user": { 280 | "id": 4, 281 | "name": "Mary Anne" 282 | } 283 | } 284 | }>>, 285 | 286 | #---------------------------------------------------------------------- 287 | '@include directive if variable false', 288 | 289 | 'query ($x: Boolean) { user(id: 4) { id, name @include(if: $x)} }', 290 | 291 | { x => False }, 292 | 293 | Q<<{ 294 | "data": { 295 | "user": { 296 | "id": 4 297 | } 298 | } 299 | }>>, 300 | 301 | #---------------------------------------------------------------------- 302 | '@include directive if variable true', 303 | 304 | 'query ($x: Boolean) { user(id: 4) { id, name @include(if: $x)} }', 305 | 306 | {x => True}, 307 | 308 | Q<<{ 309 | "data": { 310 | "user": { 311 | "id": 4, 312 | "name": "Mary Anne" 313 | } 314 | } 315 | }>>, 316 | ; 317 | 318 | for @testcases -> $description, $query, %variables, $expected 319 | { 320 | ok my $document = $schema.document($query), "parse $description"; 321 | 322 | ok my $ret = $schema.execute(:$document, :%variables), 323 | "execute $description"; 324 | 325 | is $ret.to-json, $expected, "compare $description"; 326 | } 327 | 328 | done-testing; 329 | -------------------------------------------------------------------------------- /t/07-mutations.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use lib 'lib'; 4 | use GraphQL; 5 | 6 | use Test; 7 | 8 | ok my $schema = GraphQL::Schema.new(' 9 | type User { 10 | id: ID 11 | name: String 12 | birthday: String 13 | status: Boolean 14 | } 15 | 16 | type Root { 17 | allusers(start: ID = 0, count: ID = 1): [User] 18 | user(id: ID): User 19 | } 20 | 21 | input UserInput { 22 | name: String 23 | birthday: String 24 | status: Boolean 25 | } 26 | 27 | type Changes { 28 | adduser(newuser: UserInput): ID 29 | updateuser(id: ID, userinput: UserInput): User 30 | } 31 | 32 | schema { 33 | query: Root 34 | mutation: Changes 35 | } 36 | '), 'Build Schema'; 37 | 38 | class User 39 | { 40 | has $.id is rw; 41 | has $.name is rw; 42 | has $.birthday is rw; 43 | has $.status is rw; 44 | } 45 | 46 | my @users = 47 | User.new(id => "0", 48 | name => 'Gilligan', 49 | birthday => 'Friday', 50 | status => True), 51 | User.new(id => "1", 52 | name => 'Skipper', 53 | birthday => 'Monday', 54 | status => False), 55 | User.new(id => "2", 56 | name => 'Professor', 57 | birthday => 'Tuesday', 58 | status => True), 59 | User.new(id => "3", 60 | name => 'Ginger', 61 | birthday => 'Wednesday', 62 | status => True), 63 | User.new(id => "4", 64 | name => 'Mary Anne', 65 | birthday => 'Thursday', 66 | status => True); 67 | 68 | $schema.resolvers( 69 | { 70 | Root => 71 | { 72 | allusers => 73 | sub (Int :$start, Int :$count) 74 | { 75 | @users[$start ..^ $start+$count] 76 | }, 77 | user => 78 | sub (Int :$id) 79 | { 80 | @users[$id] 81 | } 82 | }, 83 | Changes => 84 | { 85 | adduser => sub (:%newuser) 86 | { 87 | push @users, User.new(id => @users.elems.Str, |%newuser); 88 | return @users.elems - 1; 89 | }, 90 | updateuser => sub (Int :$id, :%userinput) 91 | { 92 | for %userinput.kv -> $k, $v 93 | { 94 | @users[$id]."$k"() = $v; 95 | } 96 | 97 | @users[$id] 98 | } 99 | } 100 | }); 101 | 102 | my @testcases = 103 | 'Get user 3', 104 | 105 | '{ user(id: 3) { id, name } }', 106 | 107 | {}, 108 | 109 | Q<<{ 110 | "data": { 111 | "user": { 112 | "id": "3", 113 | "name": "Ginger" 114 | } 115 | } 116 | }>>, 117 | 118 | #---------------------------------------------------------------------- 119 | 'Update user 3', 120 | 121 | 'mutation { updateuser(id: 3, userinput: { name: "Fred" }) { id, name } }', 122 | 123 | {}, 124 | 125 | Q<<{ 126 | "data": { 127 | "updateuser": { 128 | "id": "3", 129 | "name": "Fred" 130 | } 131 | } 132 | }>>, 133 | 134 | #---------------------------------------------------------------------- 135 | 'Get changed user 3', 136 | 137 | '{ user(id: 3) { id, name } }', 138 | 139 | {}, 140 | 141 | Q<<{ 142 | "data": { 143 | "user": { 144 | "id": "3", 145 | "name": "Fred" 146 | } 147 | } 148 | }>>, 149 | 150 | #---------------------------------------------------------------------- 151 | 'Update user 3, change multiple fields', 152 | 153 | 'mutation { updateuser(id: 3, 154 | userinput: { name: "Fred", birthday: "Saturday", status: false }) 155 | { id, birthday, status, name } 156 | }', 157 | 158 | {}, 159 | 160 | Q<<{ 161 | "data": { 162 | "updateuser": { 163 | "id": "3", 164 | "birthday": "Saturday", 165 | "status": false, 166 | "name": "Fred" 167 | } 168 | } 169 | }>>, 170 | 171 | #---------------------------------------------------------------------- 172 | 'Change user 2 with variable for userinput', 173 | 174 | 'mutation ($updateuser: UserInput) { 175 | updateuser(id: 2, userinput: $updateuser) 176 | { id, birthday, status, name } 177 | }', 178 | 179 | { 180 | updateuser => { 181 | name => 'John', 182 | birthday => 'Sunday' 183 | } 184 | }, 185 | 186 | Q<<{ 187 | "data": { 188 | "updateuser": { 189 | "id": "2", 190 | "birthday": "Sunday", 191 | "status": true, 192 | "name": "John" 193 | } 194 | } 195 | }>>, 196 | 197 | #---------------------------------------------------------------------- 198 | 'Insert a new user', 199 | 200 | 'mutation ($newuser: UserInput) { 201 | adduser(newuser: $newuser) 202 | }', 203 | 204 | { 205 | newuser => { 206 | name => 'Thurston', 207 | birthday => 'Tuesday', 208 | status => 'false' 209 | } 210 | }, 211 | 212 | Q<<{ 213 | "data": { 214 | "adduser": 5 215 | } 216 | }>>, 217 | 218 | #---------------------------------------------------------------------- 219 | 'Check to see if new user present', 220 | 221 | '{ user(id: 5) { id, birthday, status, name } }', 222 | 223 | {}, 224 | 225 | Q<<{ 226 | "data": { 227 | "user": { 228 | "id": "5", 229 | "birthday": "Tuesday", 230 | "status": true, 231 | "name": "Thurston" 232 | } 233 | } 234 | }>>, 235 | 236 | ; 237 | 238 | for @testcases -> $description, $query, %variables, $expected 239 | { 240 | ok my $document = $schema.document($query), 241 | "parse $description"; 242 | 243 | ok my $ret = $schema.execute(:$document, :%variables), 244 | "execute $description"; 245 | 246 | is $ret.to-json, $expected, "compare $description"; 247 | } 248 | 249 | done-testing; 250 | -------------------------------------------------------------------------------- /t/08-schemaobjects.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use GraphQL; 3 | use GraphQL::Actions; 4 | use GraphQL::Types; 5 | use Test; 6 | 7 | my $schema = GraphQL::Schema.new; 8 | 9 | my $actions = GraphQL::Actions.new(:$schema); 10 | 11 | is GraphQL::Grammar.parse('testname', :$actions, rule => 'Name').made, 12 | 'testname', 'Name'; 13 | 14 | is GraphQL::Grammar.parse('27', :$actions, rule => 'Value').made, 15 | 27, 'Int Value'; 16 | 17 | is GraphQL::Grammar.parse('-27', :$actions, rule => 'Value').made, 18 | -27, 'Negative Int Value'; 19 | 20 | is GraphQL::Grammar.parse('27.47', :$actions, rule => 'Value').made, 21 | 27.47, 'Float Value'; 22 | 23 | is GraphQL::Grammar.parse('-27.47', :$actions, rule => 'Value').made, 24 | -27.47, 'Negative Float Value'; 25 | 26 | is GraphQL::Grammar.parse('27e47', :$actions, rule => 'Value').made, 27 | 27e47, 'Exp Float Value'; 28 | 29 | is GraphQL::Grammar.parse('"foo"', :$actions, rule => 'Value').made, 30 | 'foo', 'String Value'; 31 | 32 | is GraphQL::Grammar.parse('"f\"oo"', :$actions, rule => 'Value').made, 33 | 'f"oo', 'String Value, escaped "'; 34 | 35 | is GraphQL::Grammar.parse('"☺"', :$actions, rule => 'Value').made, 36 | '☺', 'Unicode String Value'; 37 | 38 | is GraphQL::Grammar.parse('"this \u263a is fun!"', 39 | :$actions, rule => 'Value').made, 40 | 'this ☺ is fun!', 'Unicode String Value'; 41 | 42 | is GraphQL::Grammar.parse('true', :$actions, rule => 'Value').made, 43 | True, 'Boolean True'; 44 | 45 | is GraphQL::Grammar.parse('false', :$actions, rule => 'Value').made, 46 | False, 'Boolean False'; 47 | 48 | is GraphQL::Grammar.parse('null', :$actions, rule => 'Value').made, 49 | Nil, 'null'; 50 | 51 | is-deeply GraphQL::Grammar.parse('$var', :$actions, rule => 'Value').made, 52 | GraphQL::Variable.new(name => 'var'), 'Variable'; 53 | 54 | is-deeply GraphQL::Grammar.parse('interface Entity { id: ID! name: String }', 55 | :$actions, rule => 'Interface').made, 56 | GraphQL::Interface.new( 57 | name => 'Entity', 58 | fieldlist => ( 59 | GraphQL::Field.new( 60 | name => 'id', 61 | type => GraphQL::Non-Null.new(ofType => GraphQLID), 62 | ), 63 | GraphQL::Field.new( 64 | name => 'name', 65 | type => GraphQLString 66 | ) 67 | ) 68 | ), 'Interface'; 69 | 70 | is-deeply GraphQL::Grammar.parse('scalar Url', 71 | :$actions, rule => 'Scalar').made, 72 | GraphQL::Scalar.new(name => 'Url'), 'Scalar'; 73 | 74 | is-deeply GraphQL::Grammar.parse('enum USER_STATE { NOT_FOUND ACTIVE 75 | INACTIVE SUSPENDED }', 76 | :$actions, rule => 'Enum').made, 77 | GraphQL::Enum.new(name => 'USER_STATE', 78 | enumValues => [ 79 | GraphQL::EnumValue.new(name => 'NOT_FOUND'), 80 | GraphQL::EnumValue.new(name => 'ACTIVE'), 81 | GraphQL::EnumValue.new(name => 'INACTIVE'), 82 | GraphQL::EnumValue.new(name => 'SUSPENDED')]), 83 | 'Enum'; 84 | 85 | done-testing; 86 | -------------------------------------------------------------------------------- /t/09-errors.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use GraphQL; 4 | use Test; 5 | 6 | ok my $schema = GraphQL::Schema.new('type Query { hello: String }', 7 | resolvers => { Query => { hello => sub { 'Hello World' } } }), 8 | 'Make basic schema'; 9 | 10 | 11 | ok my $ret = $schema.execute('{badfield}'), 'Bad Field Query'; 12 | 13 | is $ret.to-json, Q<{ 14 | "errors": [ 15 | { 16 | "message": "Field badfield not defined" 17 | } 18 | ] 19 | }>, 'Bad Field Error'; 20 | 21 | done-testing; 22 | -------------------------------------------------------------------------------- /t/10-validation.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use Test; 4 | 5 | use GraphQL; 6 | 7 | my $schema = GraphQL::Schema.new(' 8 | enum DogCommand { SIT, DOWN, HEEL } 9 | 10 | type Dog implements Pet { 11 | name: String! 12 | nickname: String 13 | barkVolume: Int 14 | doesKnowCommand(dogCommand: DogCommand!): Boolean! 15 | isHousetrained(atOtherHomes: Boolean): Boolean! 16 | owner: Human 17 | } 18 | 19 | interface Sentient { 20 | name: String! 21 | } 22 | 23 | interface Pet { 24 | name: String! 25 | } 26 | 27 | type Alien implements Sentient { 28 | name: String! 29 | homePlanet: String 30 | } 31 | 32 | type Human implements Sentient { 33 | name: String! 34 | } 35 | 36 | enum CatCommand { JUMP } 37 | 38 | type Cat implements Pet { 39 | name: String! 40 | nickname: String 41 | doesKnowCommand(catCommand: CatCommand!): Boolean! 42 | meowVolume: Int 43 | } 44 | 45 | union CatOrDog = Cat | Dog 46 | union DogOrHuman = Dog | Human 47 | union HumanOrAlien = Human | Alien 48 | 49 | type Query { 50 | dog: Dog 51 | } 52 | '); 53 | 54 | #say $schema.Str; 55 | 56 | ok $schema.document(' 57 | query getDogName { 58 | dog { 59 | name 60 | } 61 | } 62 | 63 | query getOwnerName { 64 | dog { 65 | owner { 66 | name 67 | } 68 | } 69 | } 70 | '), 'Valid Operation Name Uniqueness'; 71 | 72 | nok try { $schema.document(' 73 | query getName { 74 | dog { 75 | name 76 | } 77 | } 78 | 79 | query getName { 80 | dog { 81 | owner { 82 | name 83 | } 84 | } 85 | } 86 | ') }, 'Invalid Operation Name Uniqueness'; 87 | 88 | nok try { $schema.document(' 89 | query dogOperation { 90 | dog { 91 | name 92 | } 93 | } 94 | 95 | mutation dogOperation { 96 | mutateDog { 97 | id 98 | } 99 | }') }, 'Invalid Operation Name Uniqueness query/mutation'; 100 | 101 | ok $schema.document(' 102 | { 103 | dog { 104 | name 105 | } 106 | }'), 'Lone Anonymous Operation'; 107 | 108 | nok try { $schema.document(' 109 | { 110 | dog { 111 | name 112 | } 113 | } 114 | 115 | query getName { 116 | dog { 117 | owner { 118 | name 119 | } 120 | } 121 | } 122 | 123 | '); }, 'Invalid Lone Anonymous Operation'; 124 | 125 | nok try { $schema.document(' 126 | fragment fieldNotDefined on Dog { 127 | meowVolume 128 | }') }, 'Fields Selection, field not defined'; 129 | 130 | nok try { $schema.document(' 131 | fragment aliasedLyingFieldTargetNotDefined on Dog { 132 | barkVolume: kawVolume 133 | }') }, 'Field Selections, aliased target field must be defined on scoped type'; 134 | 135 | ok $schema.document(' 136 | fragment interfaceFieldSelection on Pet { 137 | name 138 | }'), 'Field selection on interface'; 139 | 140 | nok try { $schema.document(' 141 | fragment definedOnImplementorsButNotInterface on Pet { 142 | nickname 143 | }') }, 'Field not defined on interface'; 144 | 145 | ok $schema.document(' 146 | fragment inDirectFieldSelectionOnUnion on CatOrDog { 147 | __typename 148 | ... on Pet { 149 | name 150 | } 151 | ... on Dog { 152 | barkVolume 153 | } 154 | }'), 'inDirect Field Selection on Union'; 155 | 156 | nok try { $schema.document(' 157 | fragment directFieldSelectionOnUnion on CatOrDog { 158 | name 159 | barkVolume 160 | }') }, 'direct field selection on union'; 161 | 162 | ok $schema.document(' 163 | fragment mergeIdenticalFields on Dog { 164 | name 165 | name 166 | }'), 'Merge Identical Fields'; 167 | 168 | ok $schema.document(' 169 | fragment mergeIdenticalAliasesAndFields on Dog { 170 | otherName: name 171 | otherName: name 172 | }'), 'Merge Identical Aliases and Fields'; 173 | 174 | #nok $schema.document(' 175 | #fragment conflictingBecauseAlias on Dog { 176 | # name: nickname 177 | # name 178 | #}'), 'Conflicting because alias'; 179 | 180 | 181 | done-testing; 182 | -------------------------------------------------------------------------------- /t/11-abstract-types.t: -------------------------------------------------------------------------------- 1 | use Test; 2 | 3 | use GraphQL; 4 | 5 | class Dog 6 | { 7 | has $.name; 8 | has $.nickname; 9 | has $.barkVolume; 10 | } 11 | 12 | class Cat 13 | { 14 | has $.name; 15 | has $.meowVolume; 16 | } 17 | 18 | my $schema = GraphQL::Schema.new(' 19 | interface Pet { 20 | name: String! 21 | } 22 | 23 | type Dog implements Pet { 24 | name: String! 25 | nickname: String 26 | barkVolume: Int 27 | } 28 | 29 | type Cat implements Pet { 30 | name: String! 31 | meowVolume: Int 32 | } 33 | 34 | union CatOrDog = Cat | Dog 35 | 36 | type Query { 37 | pet(kind: Boolean): CatOrDog 38 | }', 39 | resolvers => 40 | { 41 | Query => 42 | { 43 | pet => sub (:$kind) 44 | { 45 | $kind 46 | ?? Cat.new(:name('Fluffy'), :meowVolume(17)) 47 | !! Dog.new(:name('Fido'), :nickname('Bruiser'), :barkVolume(22)) 48 | } 49 | } 50 | }); 51 | 52 | is $schema.execute(' 53 | { 54 | pet(kind: false) { 55 | name 56 | ... on Cat { 57 | meowVolume 58 | } 59 | ... on Dog { 60 | nickname 61 | barkVolume 62 | } 63 | } 64 | }').to-json, 65 | '{ 66 | "data": { 67 | "pet": { 68 | "name": "Fido", 69 | "nickname": "Bruiser", 70 | "barkVolume": 22 71 | } 72 | } 73 | }', 'Union with type specific fragment, false'; 74 | 75 | is $schema.execute(' 76 | { 77 | pet(kind: true) { 78 | name 79 | ... on Cat { 80 | meowVolume 81 | } 82 | ... on Dog { 83 | nickname 84 | barkVolume 85 | } 86 | } 87 | }').to-json, 88 | '{ 89 | "data": { 90 | "pet": { 91 | "name": "Fluffy", 92 | "meowVolume": 17 93 | } 94 | } 95 | }', 'Union with type specific fragment, true'; 96 | 97 | done-testing; 98 | 99 | -------------------------------------------------------------------------------- /xt/01-my-meta.t: -------------------------------------------------------------------------------- 1 | #!perl6 2 | 3 | use v6; 4 | use lib 'lib'; 5 | 6 | use Test; 7 | use Test::META; 8 | 9 | plan 1; 10 | 11 | meta-ok(); 12 | 13 | done-testing; 14 | --------------------------------------------------------------------------------