graphlient 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  4. data/.github/workflows/ci.yml +33 -30
  5. data/.github/workflows/danger-comment.yml +10 -0
  6. data/.github/workflows/danger.yml +13 -22
  7. data/.github/workflows/rubocop.yml +19 -19
  8. data/.gitignore +59 -52
  9. data/.rspec +1 -1
  10. data/.rubocop.yml +26 -28
  11. data/.rubocop_todo.yml +72 -48
  12. data/CHANGELOG.md +128 -113
  13. data/CONTRIBUTING.md +125 -125
  14. data/Dangerfile +24 -24
  15. data/Gemfile +35 -27
  16. data/LICENSE +21 -21
  17. data/README.md +800 -533
  18. data/RELEASING.md +63 -63
  19. data/Rakefile +15 -15
  20. data/UPGRADING.md +23 -23
  21. data/graphlient.gemspec +19 -19
  22. data/lib/graphlient/adapters/http/adapter.rb +41 -41
  23. data/lib/graphlient/adapters/http/faraday_adapter.rb +79 -79
  24. data/lib/graphlient/adapters/http/http_adapter.rb +40 -39
  25. data/lib/graphlient/adapters/http.rb +3 -3
  26. data/lib/graphlient/adapters.rb +1 -1
  27. data/lib/graphlient/client.rb +98 -80
  28. data/lib/graphlient/errors/client_error.rb +6 -6
  29. data/lib/graphlient/errors/connection_failed_error.rb +6 -6
  30. data/lib/graphlient/errors/error.rb +13 -12
  31. data/lib/graphlient/errors/execution_error.rb +29 -29
  32. data/lib/graphlient/errors/faraday_server_error.rb +12 -12
  33. data/lib/graphlient/errors/graphql_error.rb +53 -52
  34. data/lib/graphlient/errors/http_options_error.rb +6 -6
  35. data/lib/graphlient/errors/http_server_error.rb +13 -13
  36. data/lib/graphlient/errors/server_error.rb +7 -7
  37. data/lib/graphlient/errors/timeout_error.rb +6 -6
  38. data/lib/graphlient/errors.rb +10 -10
  39. data/lib/graphlient/extensions/query.rb +15 -15
  40. data/lib/graphlient/extensions.rb +1 -1
  41. data/lib/graphlient/query/directive.rb +47 -0
  42. data/lib/graphlient/query/serializer/arguments.rb +67 -0
  43. data/lib/graphlient/query/serializer/directives.rb +17 -0
  44. data/lib/graphlient/query/serializer/evaluator.rb +15 -0
  45. data/lib/graphlient/query/serializer/fragments.rb +28 -0
  46. data/lib/graphlient/query/serializer/scalars.rb +63 -0
  47. data/lib/graphlient/query/serializer.rb +153 -0
  48. data/lib/graphlient/query.rb +29 -128
  49. data/lib/graphlient/schema.rb +27 -26
  50. data/lib/graphlient/version.rb +3 -3
  51. data/lib/graphlient.rb +8 -8
  52. data/spec/graphlient/adapters/http/faraday_adapter_spec.rb +172 -167
  53. data/spec/graphlient/adapters/http/http_adapter_spec.rb +60 -60
  54. data/spec/graphlient/client_dsl_spec.rb +153 -0
  55. data/spec/graphlient/client_query_spec.rb +369 -346
  56. data/spec/graphlient/client_schema_spec.rb +75 -75
  57. data/spec/graphlient/extensions/query_spec.rb +16 -16
  58. data/spec/graphlient/github_query_spec.rb +32 -32
  59. data/spec/graphlient/query_dsl_spec.rb +231 -0
  60. data/spec/graphlient/query_spec.rb +105 -105
  61. data/spec/graphlient/schema_spec.rb +56 -56
  62. data/spec/graphlient/static_client_query_spec.rb +75 -68
  63. data/spec/graphlient/webmock_client_query_spec.rb +41 -41
  64. data/spec/spec_helper.rb +26 -14
  65. data/spec/support/context/dummy_client.rb +33 -26
  66. data/spec/support/context/github_client.rb +18 -18
  67. data/spec/support/dummy_app.rb +21 -19
  68. data/spec/support/dummy_schema.rb +17 -17
  69. data/spec/support/fixtures/github/schema.yml +14282 -14282
  70. data/spec/support/fixtures/github/user.yml +76 -76
  71. data/spec/support/fixtures/github/viewer.yml +76 -76
  72. data/spec/support/fixtures/invoice_api.json +1288 -1288
  73. data/spec/support/mutations/create_invoice.rb +18 -18
  74. data/spec/support/queries/query.rb +48 -47
  75. data/spec/support/schema/github.json +44479 -44479
  76. data/spec/support/types/invoice_type.rb +13 -13
  77. data/spec/support/types/mutation_type.rb +5 -5
  78. data/spec/support/vcr.rb +9 -9
  79. metadata +13 -8
  80. data/Gemfile.danger +0 -5
data/README.md CHANGED
@@ -1,533 +1,800 @@
1
- # Graphlient
2
-
3
- [![Gem Version](https://badge.fury.io/rb/graphlient.svg)](https://badge.fury.io/rb/graphlient)
4
- [![Build Status](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml/badge.svg)](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml)
5
-
6
- A friendlier Ruby client for consuming GraphQL-based APIs. Built on top of your usual [graphql-client](https://github.com/github/graphql-client), but with better defaults, more consistent error handling, and using the [faraday](https://github.com/lostisland/faraday) HTTP client.
7
-
8
- # Table of Contents
9
-
10
- - [Installation](#installation)
11
- - [Usage](#usage)
12
- - [Schema Storing and Loading on Disk](#schema-storing-and-loading-on-disk)
13
- - [Preloading Schema Once](#preloading-schema-once)
14
- - [Error Handling](#error-handling)
15
- - [Executing Parameterized Queries and Mutations](#executing-parameterized-queries-and-mutations)
16
- - [Parse and Execute Queries Separately](#parse-and-execute-queries-separately)
17
- - [Dynamic vs. Static Queries](#dynamic-vs-static-queries)
18
- - [Generate Queries with Graphlient::Query](#generate-queries-with-graphlientquery)
19
- - [Create API Client Classes with Graphlient::Extension::Query](#create-api-client-classes-with-graphlientextensionquery)
20
- - [Swapping the HTTP Stack](#swapping-the-http-stack)
21
- - [Testing with Graphlient and RSpec](#testing-with-graphlient-and-rspec)
22
- - [License](#license)
23
-
24
- ## Installation
25
-
26
- Add the following line to your Gemfile.
27
-
28
- ```ruby
29
- gem 'graphlient'
30
- ```
31
-
32
- ## Usage
33
-
34
- Create a new instance of `Graphlient::Client` with a URL and optional headers/http_options.
35
-
36
- ```ruby
37
- client = Graphlient::Client.new('https://test-graphql.biz/graphql',
38
- headers: {
39
- 'Authorization' => 'Bearer 123'
40
- },
41
- http_options: {
42
- read_timeout: 20,
43
- write_timeout: 30
44
- }
45
- )
46
- ```
47
-
48
- | http_options | default | type |
49
- |---------------|---------|---------|
50
- | read_timeout | nil | seconds |
51
- | write_timeout | nil | seconds |
52
-
53
- The schema is available automatically via `.schema`.
54
-
55
- ```ruby
56
- client.schema # GraphQL::Schema
57
- ```
58
-
59
- Make queries with `query`, which takes a String or a block for the query definition.
60
-
61
- With a String.
62
-
63
- ```ruby
64
- response = client.query <<~GRAPHQL
65
- query {
66
- invoice(id: 10) {
67
- id
68
- total
69
- line_items {
70
- price
71
- item_type
72
- }
73
- }
74
- }
75
- GRAPHQL
76
- ```
77
-
78
- With a block.
79
-
80
- ```ruby
81
- response = client.query do
82
- query do
83
- invoice(id: 10) do
84
- id
85
- total
86
- line_items do
87
- price
88
- item_type
89
- end
90
- end
91
- end
92
- end
93
- ```
94
-
95
- This will call the endpoint setup in the configuration with `POST`, the `Authorization` header and `query` as follows.
96
-
97
- ```graphql
98
- query {
99
- invoice(id: 10) {
100
- id
101
- total
102
- line_items {
103
- price
104
- item_type
105
- }
106
- }
107
- }
108
- ```
109
-
110
- A successful response object always contains data which can be iterated upon. The following example returns the first line item's price.
111
-
112
- ```ruby
113
- response.data.invoice.line_items.first.price
114
- ```
115
-
116
- You can also execute mutations the same way.
117
-
118
- ```ruby
119
- response = client.query do
120
- mutation do
121
- createInvoice(input: { fee_in_cents: 12_345 }) do
122
- id
123
- fee_in_cents
124
- end
125
- end
126
- end
127
- ```
128
-
129
- The successful response contains data in `response.data`. The following example returns the newly created invoice's ID.
130
-
131
- ```ruby
132
- response.data.create_invoice.first.id
133
- ```
134
-
135
- ### Schema storing and loading on disk
136
-
137
- To reduce requests to graphql API you can cache schema:
138
-
139
- ```ruby
140
- client = Client.new(url, schema_path: 'config/your_graphql_schema.json')
141
- client.schema.dump! # you only need to call this when graphql schema changes
142
- ```
143
-
144
- ### Preloading Schema Once
145
-
146
- Even if caching the schema on disk, instantiating `Graphlient::Client` often can be both time and memory intensive due to loading the schema for each instance. This is especially true if the schema is a large file. To get around these performance issues, instantiate your schema once and pass it in as a configuration option.
147
-
148
- One time in an initializer
149
-
150
- ```ruby
151
- schema = Graphlient::Schema.new(
152
- 'https://graphql.foo.com/graphql', 'lib/graphql_schema_foo.json'
153
- )
154
- ```
155
-
156
- Pass in each time you initialize a client
157
-
158
- ```
159
- client = Graphlient::Client.new(
160
- 'https://graphql.foo.com/graphql',
161
- schema: schema,
162
- headers: {
163
- 'Authorization' => 'Bearer 123',
164
- }
165
- )
166
- ```
167
-
168
- ### Error Handling
169
-
170
- Unlike graphql-client, Graphlient will always raise an exception unless the query has succeeded.
171
-
172
- * [Graphlient::Errors::ClientError](lib/graphlient/errors/client_error.rb): all client-side query validation failures based on current schema
173
- * [Graphlient::Errors::GraphQLError](lib/graphlient/errors/graphql_error.rb): all GraphQL API errors, with a humanly readable collection of problems
174
- * [Graphlient::Errors::ExecutionError](lib/graphlient/errors/execution_error.rb): all GraphQL execution errors, with a humanly readable collection of problems
175
- * [Graphlient::Errors::ServerError](lib/graphlient/errors/server_error.rb): all transport errors raised by HTTP Adapters. You can access `inner_exception`, `status_code` and `response` on these errors to get more details on what went wrong
176
- * [Graphlient::Errors::FaradayServerError](lib/graphlient/errors/faraday_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
177
- * [Graphlient::Errors::HttpServerError](lib/graphlient/errors/http_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
178
- * [Graphlient::Errors::ConnectionFailedError](lib/graphlient/errors/connection_failed_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
179
- * [Graphlient::Errors::TimeoutError](lib/graphlient/errors/timeout_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
180
- * [Graphlient::Errors::HttpOptionsError](lib/graphlient/errors/http_options_error.rb): all NoMethodError raised by HTTP Adapters when given options in `http_options` are invalid
181
-
182
-
183
- All errors inherit from `Graphlient::Errors::Error` if you need to handle them in bulk.
184
-
185
- ### Executing Parameterized Queries and Mutations
186
-
187
- Graphlient can execute parameterized queries and mutations by providing variables as query parameters.
188
-
189
- The following query accepts an array of IDs.
190
-
191
- With a String.
192
-
193
- ```ruby
194
- query = <<-GRAPHQL
195
- query($ids: [Int]) {
196
- invoices(ids: $ids) {
197
- id
198
- fee_in_cents
199
- }
200
- }
201
- GRAPHQL
202
- variables = { ids: [42] }
203
-
204
- client.query(query, variables)
205
- ```
206
-
207
- With a block.
208
-
209
- ```ruby
210
- client.query(ids: [42]) do
211
- query(ids: [:int]) do
212
- invoices(ids: :ids) do
213
- id
214
- fee_in_cents
215
- end
216
- end
217
- end
218
- ```
219
- Graphlient supports following Scalar types for parameterized queries by default:
220
- - `:id` maps to `ID`
221
- - `:boolean` maps to `Boolean`
222
- - `:float` maps to `Float`
223
- - `:int` maps to `Int`
224
- - `:string` maps to `String`
225
-
226
- You can use any of the above types with `!` to make it required or use them in `[]` for array parameters.
227
-
228
- For any other custom types, graphlient will simply use `to_s` of the symbol provided for the type, so `query(ids: [:InvoiceType!])` will result in `query($ids: [InvoiceType!])`.
229
-
230
- The following mutation accepts a custom type that requires `fee_in_cents`.
231
-
232
- ```ruby
233
- client.query(input: { fee_in_cents: 12_345 }) do
234
- mutation(input: :createInvoiceInput!) do
235
- createInvoice(input: :input) do
236
- id
237
- fee_in_cents
238
- end
239
- end
240
- end
241
- ```
242
-
243
- ### Parse and Execute Queries Separately
244
-
245
- You can `parse` and `execute` queries separately with optional variables. This is highly recommended as parsing a query and validating a query on every request adds performance overhead. Parsing queries early allows validation errors to be discovered before request time and avoids many potential security issues.
246
-
247
-
248
- ```ruby
249
- # parse a query, returns a GraphQL::Client::OperationDefinition
250
- query = client.parse do
251
- query(ids: [:int]) do
252
- invoices(ids: :ids) do
253
- id
254
- fee_in_cents
255
- end
256
- end
257
- end
258
-
259
- # execute a query, returns a GraphQL::Client::Response
260
- client.execute query, ids: [42]
261
- ```
262
-
263
- Or pass in a string instead of a block:
264
-
265
- ```ruby
266
- # parse a query, returns a GraphQL::Client::OperationDefinition
267
- query = client.parse <<~GRAPHQL
268
- query($some_id: Int) {
269
- invoice(id: $some_id) {
270
- id
271
- feeInCents
272
- }
273
- }
274
- GRAPHQL
275
-
276
- # execute a query, returns a GraphQL::Client::Response
277
- client.execute query, ids: [42]
278
- ```
279
-
280
- ### Dynamic vs. Static Queries
281
-
282
- Graphlient uses [graphql-client](https://github.com/github/graphql-client), which [recommends](https://github.com/github/graphql-client/blob/master/guides/dynamic-query-error.md) building queries as static module members along with dynamic variables during execution. This can be accomplished with graphlient the same way.
283
-
284
- Create a new instance of `Graphlient::Client` with a URL and optional headers.
285
-
286
- ```ruby
287
- module SWAPI
288
- Client = Graphlient::Client.new('https://test-graphql.biz/graphql',
289
- headers: {
290
- 'Authorization' => 'Bearer 123'
291
- },
292
- allow_dynamic_queries: false
293
- )
294
- end
295
- ```
296
-
297
- The schema is available automatically via `.schema`.
298
-
299
- ```ruby
300
- SWAPI::Client.schema # GraphQL::Schema
301
- ```
302
-
303
- Define a query.
304
-
305
- ```ruby
306
- module SWAPI
307
- InvoiceQuery = Client.parse do
308
- query(id: :int) do
309
- invoice(id: :id) do
310
- id
311
- fee_in_cents
312
- end
313
- end
314
- end
315
- end
316
- ```
317
-
318
- Execute the query.
319
-
320
- ```ruby
321
- response = SWAPI::Client.execute(SWAPI::InvoiceQuery, id: 42)
322
- ```
323
-
324
- Note that in the example above the client is created with `allow_dynamic_queries: false` (only allow static queries), while graphlient defaults to `allow_dynamic_queries: true` (allow dynamic queries). This option is marked deprecated, but we're proposing to remove it and default it to `true` in [graphql-client#128](https://github.com/github/graphql-client/issues/128).
325
-
326
- ### Generate Queries with Graphlient::Query
327
-
328
- You can directly use `Graphlient::Query` to generate raw GraphQL queries.
329
-
330
- ```ruby
331
- query = Graphlient::Query.new do
332
- query do
333
- invoice(id: 10) do
334
- line_items
335
- end
336
- end
337
- end
338
-
339
- query.to_s
340
- # "\nquery {\n invoice(id: 10){\n line_items\n }\n }\n"
341
- ```
342
-
343
- ### Use of Fragments
344
-
345
- [Fragments](https://github.com/github/graphql-client#defining-queries) should be referred by constant:
346
-
347
- ```ruby
348
- module Fragments
349
- Invoice = client.parse <<~'GRAPHQL'
350
- fragment on Invoice {
351
- id
352
- feeInCents
353
- }
354
- GRAPHQL
355
- end
356
- ```
357
-
358
- `Graphlient` offers the syntax below to refer to the original constant:
359
- * Triple underscore `___` to refer to the fragment
360
- * Double underscore `__` for namespace separator
361
-
362
- In this example, `Fragments::Invoice` would be referred as follows:
363
-
364
- ```ruby
365
- invoice_query = client.parse do
366
- query do
367
- invoice(id: 10) do
368
- id
369
- ___Fragments__Invoice
370
- end
371
- end
372
- end
373
- ```
374
-
375
- The wrapped response only allows access to fields that have been explicitly asked for.
376
- In this example, while `id` has been referenced directly in the main query, `feeInCents` has been spread via fragment and trying to access it in the original wrapped response will throw [`GraphQL::Client::ImplicitlyFetchedFieldError`](https://github.com/github/graphql-client/blob/master/guides/implicitly-fetched-field-error.md) (to prevent data leaks between components).
377
-
378
- ```ruby
379
- response = client.execute(invoice_query)
380
- result = response.data.invoice
381
- result.to_h
382
- # {"id" => 10, "feeInCents"=> 20000}
383
- result.id
384
- # 10
385
- result.fee_in_cents
386
- # raises GraphQL::Client::ImplicitlyFetchedFieldError
387
- ```
388
-
389
- `feeInCents` cannot be fetched directly from the main query, but from the fragment as shown below:
390
-
391
- ```ruby
392
- invoice = Fragments::Invoice.new(result)
393
- invoice.id
394
- # 10
395
- invoice.fee_in_cents
396
- # 20000
397
- ```
398
-
399
- ### Create API Client Classes with Graphlient::Extension::Query
400
-
401
- You can include `Graphlient::Extensions::Query` in your class. This will add a new `method_missing` method to your context which will be used to generate GraphQL queries.
402
-
403
- ```ruby
404
- include Graphlient::Extensions::Query
405
-
406
- query = query do
407
- invoice(id: 10) do
408
- line_items
409
- end
410
- end
411
-
412
- query.to_s
413
- # "\nquery{\n invoice(id: 10){\n line_items\n }\n }\n"
414
- ```
415
-
416
- ### Swapping the HTTP Stack
417
-
418
- You can swap the default Faraday adapter for `Net::HTTP`.
419
-
420
- ```ruby
421
- client = Graphlient::Client.new('https://test-graphql.biz/graphql',
422
- http: Graphlient::Adapters::HTTP::HTTPAdapter
423
- )
424
- ```
425
-
426
- ### Testing with Graphlient and RSpec
427
-
428
- Use Graphlient inside your RSpec tests in a Rails application or with `Rack::Test` against your actual application.
429
-
430
- ```ruby
431
- require 'spec_helper'
432
-
433
- describe App do
434
- include Rack::Test::Methods
435
-
436
- def app
437
- # ...
438
- end
439
-
440
- let(:client) do
441
- Graphlient::Client.new('http://test-graphql.biz/graphql') do |client|
442
- client.http do |h|
443
- h.connection do |c|
444
- c.adapter Faraday::Adapter::Rack, app
445
- end
446
- end
447
- end
448
- end
449
-
450
- context 'an invoice' do
451
- let(:result) do
452
- client.query do
453
- query do
454
- invoice(id: 10) do
455
- id
456
- end
457
- end
458
- end
459
- end
460
-
461
- it 'can be retrieved' do
462
- expect(result.data.invoice.id).to eq 10
463
- end
464
- end
465
- end
466
- ```
467
-
468
- Alternately you can `stub_request` with Webmock.
469
-
470
- ```ruby
471
- describe App do
472
- let(:url) { 'http://example.com/graphql' }
473
- let(:client) { Graphlient::Client.new(url) }
474
-
475
- before do
476
- stub_request(:post, url).to_return(
477
- status: 200,
478
- body: DummySchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY).to_json
479
- )
480
- end
481
-
482
- it 'retrieves schema' do
483
- expect(client.schema).to be_a Graphlient::Schema
484
- end
485
- end
486
- ```
487
-
488
- In order to stub the response to actual queries, [dump the schema into a JSON file](#schema-storing-and-loading-on-disk) and specify it via schema_path as follows.
489
-
490
- ```ruby
491
- describe App do
492
- let(:url) { 'http://graph.biz/graphql' }
493
- let(:client) { Graphlient::Client.new(url, schema_path: 'spec/support/fixtures/invoice_api.json') }
494
- let(:query) do
495
- <<~GRAPHQL
496
- query{
497
- invoice(id: 42) {
498
- id
499
- feeInCents
500
- }
501
- }
502
- GRAPHQL
503
- end
504
- let(:json_response) do
505
- {
506
- 'data' => {
507
- 'invoice' => {
508
- 'id' => '42',
509
- 'feeInCents' => 2000
510
- }
511
- }
512
- }.to_json
513
- end
514
-
515
- before do
516
- stub_request(:post, url).to_return(
517
- status: 200,
518
- body: json_response
519
- )
520
- end
521
-
522
- it 'returns invoice fees' do
523
- response = client.query(query)
524
- expect(response.data).to be_truthy
525
- expect(response.data.invoice.id).to eq('42')
526
- expect(response.data.invoice.fee_in_cents).to eq(2000)
527
- end
528
- end
529
- ```
530
-
531
- ## License
532
-
533
- MIT License, see [LICENSE](LICENSE)
1
+ # Graphlient
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/graphlient.svg)](https://badge.fury.io/rb/graphlient)
4
+ [![Build Status](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml/badge.svg)](https://github.com/ashkan18/graphlient/actions/workflows/ci.yml)
5
+
6
+ A friendlier Ruby client for consuming GraphQL-based APIs. Built on top of your usual [graphql-client](https://github.com/github-community-projects/graphql-client), but with better defaults, more consistent error handling, and using the [faraday](https://github.com/lostisland/faraday) HTTP client.
7
+
8
+ # Table of Contents
9
+
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Schema Storing and Loading on Disk](#schema-storing-and-loading-on-disk)
13
+ - [Preloading Schema Once](#preloading-schema-once)
14
+ - [Error Handling](#error-handling)
15
+ - [Executing Parameterized Queries and Mutations](#executing-parameterized-queries-and-mutations)
16
+ - [Parse and Execute Queries Separately](#parse-and-execute-queries-separately)
17
+ - [Build Query Strings without Validation](#build-query-strings-without-validation)
18
+ - [Dynamic vs. Static Queries](#dynamic-vs-static-queries)
19
+ - [Generate Queries with Graphlient::Query](#generate-queries-with-graphlientquery)
20
+ - [Fragment Spreads and Definitions in the DSL](#fragment-spreads-and-definitions-in-the-dsl)
21
+ - [Inline Fragments in the DSL](#inline-fragments-in-the-dsl)
22
+ - [Directives in the DSL](#directives-in-the-dsl)
23
+ - [Custom Scalar Types](#custom-scalar-types)
24
+ - [Create API Client Classes with Graphlient::Extension::Query](#create-api-client-classes-with-graphlientextensionquery)
25
+ - [Swapping the HTTP Stack](#swapping-the-http-stack)
26
+ - [Testing with Graphlient and RSpec](#testing-with-graphlient-and-rspec)
27
+ - [License](#license)
28
+
29
+ ## Installation
30
+
31
+ Add the following line to your Gemfile.
32
+
33
+ ```ruby
34
+ gem 'graphlient'
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Create a new instance of `Graphlient::Client` with a URL and optional headers/http_options.
40
+
41
+ ```ruby
42
+ client = Graphlient::Client.new('https://test-graphql.biz/graphql',
43
+ headers: {
44
+ 'Authorization' => 'Bearer 123'
45
+ },
46
+ http_options: {
47
+ read_timeout: 20,
48
+ write_timeout: 30
49
+ }
50
+ )
51
+ ```
52
+
53
+ | http_options | default | type |
54
+ | ------------- | ------- | ------- |
55
+ | read_timeout | nil | seconds |
56
+ | write_timeout | nil | seconds |
57
+
58
+ The schema is available automatically via `.schema`.
59
+
60
+ ```ruby
61
+ client.schema # GraphQL::Schema
62
+ ```
63
+
64
+ Make queries with `query`, which takes a String or a block for the query definition.
65
+
66
+ With a String.
67
+
68
+ ```ruby
69
+ response = client.query <<~GRAPHQL
70
+ query {
71
+ invoice(id: 10) {
72
+ id
73
+ total
74
+ line_items {
75
+ price
76
+ item_type
77
+ }
78
+ }
79
+ }
80
+ GRAPHQL
81
+ ```
82
+
83
+ With a block.
84
+
85
+ ```ruby
86
+ response = client.query do
87
+ query do
88
+ invoice(id: 10) do
89
+ id
90
+ total
91
+ line_items do
92
+ price
93
+ item_type
94
+ end
95
+ end
96
+ end
97
+ end
98
+ ```
99
+
100
+ This will call the endpoint setup in the configuration with `POST`, the `Authorization` header and `query` as follows.
101
+
102
+ ```graphql
103
+ query {
104
+ invoice(id: 10) {
105
+ id
106
+ total
107
+ line_items {
108
+ price
109
+ item_type
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ A successful response object always contains data which can be iterated upon. The following example returns the first line item's price.
116
+
117
+ ```ruby
118
+ response.data.invoice.line_items.first.price
119
+ ```
120
+
121
+ You can also execute mutations the same way.
122
+
123
+ ```ruby
124
+ response = client.query do
125
+ mutation do
126
+ createInvoice(input: { fee_in_cents: 12_345 }) do
127
+ id
128
+ fee_in_cents
129
+ end
130
+ end
131
+ end
132
+ ```
133
+
134
+ The successful response contains data in `response.data`. The following example returns the newly created invoice's ID.
135
+
136
+ ```ruby
137
+ response.data.create_invoice.first.id
138
+ ```
139
+
140
+ ### Schema storing and loading on disk
141
+
142
+ To reduce requests to graphql API you can cache schema:
143
+
144
+ ```ruby
145
+ client = Client.new(url, schema_path: 'config/your_graphql_schema.json')
146
+ client.schema.dump! # you only need to call this when graphql schema changes
147
+ ```
148
+
149
+ ### Preloading Schema Once
150
+
151
+ Even if caching the schema on disk, instantiating `Graphlient::Client` often can be both time and memory intensive due to loading the schema for each instance. This is especially true if the schema is a large file. To get around these performance issues, instantiate your schema once and pass it in as a configuration option.
152
+
153
+ One time in an initializer
154
+
155
+ ```ruby
156
+ schema = Graphlient::Schema.new(
157
+ 'https://graphql.foo.com/graphql', 'lib/graphql_schema_foo.json'
158
+ )
159
+ ```
160
+
161
+ Pass in each time you initialize a client
162
+
163
+ ```
164
+ client = Graphlient::Client.new(
165
+ 'https://graphql.foo.com/graphql',
166
+ schema: schema,
167
+ headers: {
168
+ 'Authorization' => 'Bearer 123',
169
+ }
170
+ )
171
+ ```
172
+
173
+ ### Error Handling
174
+
175
+ Unlike graphql-client, Graphlient will always raise an exception unless the query has succeeded.
176
+
177
+ * [Graphlient::Errors::ClientError](lib/graphlient/errors/client_error.rb): all client-side query validation failures based on current schema
178
+ * [Graphlient::Errors::GraphQLError](lib/graphlient/errors/graphql_error.rb): all GraphQL API errors, with a humanly readable collection of problems
179
+ * [Graphlient::Errors::ExecutionError](lib/graphlient/errors/execution_error.rb): all GraphQL execution errors, with a humanly readable collection of problems
180
+ * [Graphlient::Errors::ServerError](lib/graphlient/errors/server_error.rb): all transport errors raised by HTTP Adapters. You can access `inner_exception`, `status_code` and `response` on these errors to get more details on what went wrong
181
+ * [Graphlient::Errors::FaradayServerError](lib/graphlient/errors/faraday_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
182
+ * [Graphlient::Errors::HttpServerError](lib/graphlient/errors/http_server_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
183
+ * [Graphlient::Errors::ConnectionFailedError](lib/graphlient/errors/connection_failed_error.rb): this inherits from `ServerError` ☝️, we recommend using `ServerError` to rescue these
184
+ * [Graphlient::Errors::TimeoutError](lib/graphlient/errors/timeout_error.rb): all client-side timeouts raised by the Faraday adapter. This does not inherit from `ServerError` because no server response or status code is available; rescue `TimeoutError` separately
185
+ * [Graphlient::Errors::HttpOptionsError](lib/graphlient/errors/http_options_error.rb): all NoMethodError raised by HTTP Adapters when given options in `http_options` are invalid
186
+
187
+
188
+ All errors inherit from `Graphlient::Errors::Error` if you need to handle them in bulk.
189
+
190
+ ### Executing Parameterized Queries and Mutations
191
+
192
+ Graphlient can execute parameterized queries and mutations by providing variables as query parameters.
193
+
194
+ The following query accepts an array of IDs.
195
+
196
+ With a String.
197
+
198
+ ```ruby
199
+ query = <<-GRAPHQL
200
+ query($ids: [Int]) {
201
+ invoices(ids: $ids) {
202
+ id
203
+ fee_in_cents
204
+ }
205
+ }
206
+ GRAPHQL
207
+ variables = { ids: [42] }
208
+
209
+ client.query(query, variables)
210
+ ```
211
+
212
+ With a block.
213
+
214
+ ```ruby
215
+ client.query(ids: [42]) do
216
+ query(ids: [:int]) do
217
+ invoices(ids: :ids) do
218
+ id
219
+ fee_in_cents
220
+ end
221
+ end
222
+ end
223
+ ```
224
+
225
+ Graphlient supports following Scalar types for parameterized queries by default:
226
+
227
+ - `:id` maps to `ID`
228
+ - `:boolean` maps to `Boolean`
229
+ - `:float` maps to `Float`
230
+ - `:int` maps to `Int`
231
+ - `:string` maps to `String`
232
+
233
+ You can use any of the above types with `!` to make it required or use them in `[]` for array parameters.
234
+
235
+ For any other custom types, graphlient will simply use `to_s` of the symbol provided for the type, so `query(ids: [:InvoiceType!])` will result in `query($ids: [InvoiceType!])`.
236
+
237
+ The following mutation accepts a custom type that requires `fee_in_cents`.
238
+
239
+ ```ruby
240
+ client.query(input: { fee_in_cents: 12_345 }) do
241
+ mutation(input: :createInvoiceInput!) do
242
+ createInvoice(input: :input) do
243
+ id
244
+ fee_in_cents
245
+ end
246
+ end
247
+ end
248
+ ```
249
+
250
+ ### Parse and Execute Queries Separately
251
+
252
+ You can `parse` and `execute` queries separately with optional variables. This is highly recommended as parsing a query and validating a query on every request adds performance overhead. Parsing queries early allows validation errors to be discovered before request time and avoids many potential security issues.
253
+
254
+ ```ruby
255
+ # parse a query, returns a GraphQL::Client::OperationDefinition
256
+ query = client.parse do
257
+ query(ids: [:int]) do
258
+ invoices(ids: :ids) do
259
+ id
260
+ fee_in_cents
261
+ end
262
+ end
263
+ end
264
+
265
+ # execute a query, returns a GraphQL::Client::Response
266
+ client.execute query, ids: [42]
267
+ ```
268
+
269
+ Or pass in a string instead of a block:
270
+
271
+ ```ruby
272
+ # parse a query, returns a GraphQL::Client::OperationDefinition
273
+ query = client.parse <<~GRAPHQL
274
+ query($some_id: Int) {
275
+ invoice(id: $some_id) {
276
+ id
277
+ feeInCents
278
+ }
279
+ }
280
+ GRAPHQL
281
+
282
+ # execute a query, returns a GraphQL::Client::Response
283
+ client.execute query, ids: [42]
284
+ ```
285
+
286
+ ### Build Query Strings without Validation
287
+
288
+ `Client#to_query_string` serializes a DSL block into a GraphQL query string and
289
+ returns it as a plain `String`. It uses the same DSL serializer (`Graphlient::Query`)
290
+ that powers `client.query` and `client.parse`, but it stops there — no schema is
291
+ loaded, no graphql-client validation runs, no HTTP call is made.
292
+
293
+ ```ruby
294
+ client = Graphlient::Client.new('https://example.com/graphql',
295
+ headers: { 'Authorization' => 'Bearer 123' }
296
+ )
297
+
298
+ query_str = client.to_query_string do
299
+ query(id: :int) do
300
+ invoice(id: :id) do
301
+ id
302
+ feeInCents
303
+ end
304
+ end
305
+ end
306
+
307
+ # => "query($id: Int){\n invoice(id: $id){\n id\n feeInCents\n }\n }"
308
+ ```
309
+
310
+ For fragment-free queries the string can be fed back to `client.execute`:
311
+
312
+ ```ruby
313
+ client.execute(query_str, id: 42)
314
+ ```
315
+
316
+ Queries containing fragment spreads (`spread :Name` → `...Name`) cannot go back
317
+ through the gem — graphql-client requires fragments to be pre-registered module
318
+ constants, not named strings. Pass those directly to your own HTTP client instead.
319
+
320
+ This is also the escape hatch if you want to replace the graphql-client dependency
321
+ entirely. `to_query_string` gives you a standard GraphQL document — from there you
322
+ own the transport: Faraday, Net::HTTP, anything else. You get full control over
323
+ headers, retries, connection pooling, and middleware without any graphql-client
324
+ overhead.
325
+
326
+ ```ruby
327
+ conn = Faraday.new('https://example.com/graphql',
328
+ headers: { 'Authorization' => 'Bearer 123', 'Content-Type' => 'application/json' }
329
+ )
330
+ response = conn.post('/', { query: query_str, variables: { id: 42 } }.to_json)
331
+ ```
332
+
333
+ ### Dynamic vs. Static Queries
334
+
335
+ Graphlient uses [graphql-client](https://github.com/github-community-projects/graphql-client), which [recommends](https://github.com/github-community-projects/graphql-client/blob/master/guides/dynamic-query-error.md) building queries as static module members along with dynamic variables during execution. This can be accomplished with graphlient the same way.
336
+
337
+ Create a new instance of `Graphlient::Client` with a URL and optional headers.
338
+
339
+ ```ruby
340
+ module SWAPI
341
+ Client = Graphlient::Client.new('https://test-graphql.biz/graphql',
342
+ headers: {
343
+ 'Authorization' => 'Bearer 123'
344
+ },
345
+ allow_dynamic_queries: false
346
+ )
347
+ end
348
+ ```
349
+
350
+ The schema is available automatically via `.schema`.
351
+
352
+ ```ruby
353
+ SWAPI::Client.schema # GraphQL::Schema
354
+ ```
355
+
356
+ Define a query.
357
+
358
+ ```ruby
359
+ module SWAPI
360
+ InvoiceQuery = Client.parse do
361
+ query(id: :int) do
362
+ invoice(id: :id) do
363
+ id
364
+ fee_in_cents
365
+ end
366
+ end
367
+ end
368
+ end
369
+ ```
370
+
371
+ Execute the query.
372
+
373
+ ```ruby
374
+ response = SWAPI::Client.execute(SWAPI::InvoiceQuery, id: 42)
375
+ ```
376
+
377
+ Note that in the example above the client is created with `allow_dynamic_queries: false` (only allow static queries), while graphlient defaults to `allow_dynamic_queries: true` (allow dynamic queries). This option is marked deprecated, but we're proposing to remove it and default it to `true` in [graphql-client#128](https://github.com/github-community-projects/graphql-client/issues/128).
378
+
379
+ ### Generate Queries with Graphlient::Query
380
+
381
+ You can directly use `Graphlient::Query` to generate raw GraphQL queries.
382
+
383
+ ```ruby
384
+ query = Graphlient::Query.new do
385
+ query do
386
+ invoice(id: 10) do
387
+ line_items
388
+ end
389
+ end
390
+ end
391
+
392
+ query.to_s
393
+ # "\nquery {\n invoice(id: 10){\n line_items\n }\n }\n"
394
+ ```
395
+
396
+ ### Fragment Spreads and Definitions in the DSL
397
+
398
+ Use `spread` to insert a named fragment spread (`...FragmentName`) in a DSL block:
399
+
400
+ ```ruby
401
+ client.query do
402
+ query do
403
+ invoice(id: 10) do
404
+ id
405
+ spread :InvoiceFields # → ...InvoiceFields
406
+ end
407
+ end
408
+ end
409
+ ```
410
+
411
+ Define the fragment body inline with `fragment` — graphlient assembles the complete
412
+ query string automatically, no external tooling needed:
413
+
414
+ ```ruby
415
+ client.query do
416
+ fragment(:InvoiceFields, on: :Invoice) do
417
+ id
418
+ feeInCents
419
+ end
420
+
421
+ query do
422
+ invoice(id: 10) do
423
+ spread :InvoiceFields
424
+ end
425
+ end
426
+ end
427
+ ```
428
+
429
+ Produces and sends:
430
+
431
+ ```graphql
432
+ query {
433
+ invoice(id: 10) {
434
+ ...InvoiceFields
435
+ }
436
+ }
437
+
438
+ fragment InvoiceFields on Invoice {
439
+ id
440
+ feeInCents
441
+ }
442
+ ```
443
+
444
+ Because the fragment is defined inline in the same query block, graphql-client treats it
445
+ as part of the same document — all fragment fields are directly accessible on the response
446
+ wrapper with no extra wrapping step:
447
+
448
+ ```ruby
449
+ response = client.query do
450
+ fragment(:InvoiceFields, on: :Invoice) do
451
+ id
452
+ feeInCents
453
+ end
454
+
455
+ query do
456
+ invoice(id: 10) do
457
+ spread :InvoiceFields
458
+ end
459
+ end
460
+ end
461
+
462
+ response.data.invoice.id # 10
463
+ response.data.invoice.fee_in_cents # 20000
464
+ ```
465
+
466
+ Multiple fragments are supported. Fragments are scoped to the query call — no global
467
+ registry, no cross-contamination between requests.
468
+
469
+ You can also apply a directive to a spread (see [Directives in the DSL](#directives-in-the-dsl)):
470
+
471
+ ```ruby
472
+ spread :InvoiceFields, _skip(if: :skip_invoice)
473
+ # → ...InvoiceFields @skip(if: $skip_invoice)
474
+ ```
475
+
476
+ ### Use of Fragments (graphql-client style)
477
+
478
+ [Fragments](https://github.com/github-community-projects/graphql-client#defining-queries) should be referred by constant:
479
+
480
+ ```ruby
481
+ module Fragments
482
+ Invoice = client.parse <<~'GRAPHQL'
483
+ fragment on Invoice {
484
+ id
485
+ feeInCents
486
+ }
487
+ GRAPHQL
488
+ end
489
+ ```
490
+
491
+ `Graphlient` offers the syntax below to refer to the original constant:
492
+ * Triple underscore `___` to refer to the fragment
493
+ * Double underscore `__` for namespace separator
494
+
495
+ In this example, `Fragments::Invoice` would be referred as follows:
496
+
497
+ ```ruby
498
+ invoice_query = client.parse do
499
+ query do
500
+ invoice(id: 10) do
501
+ id
502
+ ___Fragments__Invoice
503
+ end
504
+ end
505
+ end
506
+ ```
507
+
508
+ The wrapped response only allows access to fields that have been explicitly asked for.
509
+ In this example, while `id` has been referenced directly in the main query, `feeInCents`
510
+ has been spread via an **external fragment constant** and trying to access it in the
511
+ original wrapped response will throw
512
+ [`GraphQL::Client::ImplicitlyFetchedFieldError`](https://github.com/github-community-projects/graphql-client/blob/master/guides/implicitly-fetched-field-error.md).
513
+ This is graphql-client's component-isolation mechanism: each fragment constant "owns" the
514
+ fields it declares, preventing accidental data access across component boundaries.
515
+
516
+ ```ruby
517
+ response = client.execute(invoice_query)
518
+ result = response.data.invoice
519
+ result.to_h
520
+ # {"id" => 10, "feeInCents"=> 20000}
521
+ result.id
522
+ # 10
523
+ result.fee_in_cents
524
+ # raises GraphQL::Client::ImplicitlyFetchedFieldError
525
+ ```
526
+
527
+ `feeInCents` cannot be fetched directly from the main query, but from the fragment as shown below:
528
+
529
+ ```ruby
530
+ invoice = Fragments::Invoice.new(result)
531
+ invoice.id
532
+ # 10
533
+ invoice.fee_in_cents
534
+ # 20000
535
+ ```
536
+
537
+ > **Note:** This component-isolation behaviour only applies to external fragment constants
538
+ > (the `___` / `__` pattern). Fragments defined inline via the `fragment` DSL in the same
539
+ > query block are not subject to this restriction — their fields are accessible directly
540
+ > on the operation response (see [Fragment Spreads and Definitions in the DSL](#fragment-spreads-and-definitions-in-the-dsl)).
541
+
542
+ ### Inline Fragments in the DSL
543
+
544
+ Use `spread(on: :TypeName)` for inline fragments (`... on Type { }`), useful for union
545
+ types and interface implementations. It's the same `spread` verb as named fragment
546
+ spreads, and the same `on:` keyword as `fragment(name, on:)`:
547
+
548
+ ```ruby
549
+ client.query do
550
+ query do
551
+ invoice(id: 10) do
552
+ spread(on: :PaidInvoice) do
553
+ amountPaid
554
+ end
555
+ spread(on: :UnpaidInvoice) do
556
+ amountDue
557
+ end
558
+ end
559
+ end
560
+ end
561
+ ```
562
+
563
+ Produces:
564
+
565
+ ```graphql
566
+ query {
567
+ invoice(id: 10) {
568
+ ... on PaidInvoice {
569
+ amountPaid
570
+ }
571
+ ... on UnpaidInvoice {
572
+ amountDue
573
+ }
574
+ }
575
+ }
576
+ ```
577
+
578
+ Directives can be applied to inline fragments too (see [Directives in the DSL](#directives-in-the-dsl)):
579
+
580
+ ```ruby
581
+ spread(_skip(if: :skip_drafts), on: :DraftInvoice) { draftId }
582
+ # → ... on DraftInvoice @skip(if: $skip_drafts) { draftId }
583
+ ```
584
+
585
+ ### Directives in the DSL
586
+
587
+ Apply GraphQL directives to fields, spreads, and inline fragments using the `_name`
588
+ convention — any method starting with `_` followed by a lowercase letter is treated
589
+ as a directive (`_skip` → `@skip`, `_include` → `@include`, `_myDirective` → `@myDirective`).
590
+
591
+ **On a field:**
592
+
593
+ ```ruby
594
+ client.query(some_id: :int, skip_fee: :boolean!) do
595
+ query(some_id: :int, skip_fee: :boolean!) do
596
+ invoice(id: :some_id) do
597
+ id
598
+ feeInCents _skip(if: :skip_fee) # → feeInCents @skip(if: $skip_fee)
599
+ end
600
+ end
601
+ end
602
+ ```
603
+
604
+ **On a fragment spread:**
605
+
606
+ ```ruby
607
+ spread :InvoiceFields, _skip(if: :skip_invoice)
608
+ # → ...InvoiceFields @skip(if: $skip_invoice)
609
+ ```
610
+
611
+ **On an inline fragment:**
612
+
613
+ ```ruby
614
+ spread(_skip(if: :skip_drafts), on: :DraftInvoice) { draftId }
615
+ # → ... on DraftInvoice @skip(if: $skip_drafts) { draftId }
616
+ ```
617
+
618
+ **Multiple directives on one field:**
619
+
620
+ ```ruby
621
+ feeInCents _skip(if: :skip_fee), _include(if: :show_cents)
622
+ # → feeInCents @skip(if: $skip_fee) @include(if: $show_cents)
623
+ ```
624
+
625
+ **No-argument directive:**
626
+
627
+ ```ruby
628
+ legacyField _deprecated
629
+ # → legacyField @deprecated
630
+ ```
631
+
632
+ The directive value is a plain Ruby method call that returns a `Directive` object
633
+ before the field method runs, ensuring the directive appears in the correct position
634
+ in the output string regardless of Ruby's evaluation order.
635
+
636
+ ### Custom Scalar Types
637
+
638
+ By default, graphlient maps `:int → Int`, `:float → Float`, `:string → String`, and
639
+ `:boolean → Boolean` for variable type declarations. Register additional scalar types
640
+ in the client initialiser block:
641
+
642
+ ```ruby
643
+ client = Graphlient::Client.new('https://example.com/graphql') do |c|
644
+ c.scalar :date, 'Date'
645
+ c.scalar :uuid, 'UUID'
646
+ c.scalar :decimal, 'Decimal'
647
+ end
648
+ ```
649
+
650
+ Use the registered symbol in variable declarations:
651
+
652
+ ```ruby
653
+ client.query(created_after: Date.today.iso8601, order_id: :uuid) do
654
+ query(created_after: :date, order_id: :uuid!) do
655
+ orders(created_after: :created_after, id: :order_id) do
656
+ id
657
+ total
658
+ end
659
+ end
660
+ end
661
+ # → query($created_after: Date, $order_id: UUID!) { ... }
662
+ ```
663
+
664
+ Non-null variants work with `!`: `:date!` → `Date!`.
665
+
666
+ ### Create API Client Classes with Graphlient::Extension::Query
667
+
668
+ You can include `Graphlient::Extensions::Query` in your class. This will add a new `method_missing` method to your context which will be used to generate GraphQL queries.
669
+
670
+ ```ruby
671
+ include Graphlient::Extensions::Query
672
+
673
+ query = query do
674
+ invoice(id: 10) do
675
+ line_items
676
+ end
677
+ end
678
+
679
+ query.to_s
680
+ # "\nquery{\n invoice(id: 10){\n line_items\n }\n }\n"
681
+ ```
682
+
683
+ ### Swapping the HTTP Stack
684
+
685
+ You can swap the default Faraday adapter for `Net::HTTP`.
686
+
687
+ ```ruby
688
+ client = Graphlient::Client.new('https://test-graphql.biz/graphql',
689
+ http: Graphlient::Adapters::HTTP::HTTPAdapter
690
+ )
691
+ ```
692
+
693
+ ### Testing with Graphlient and RSpec
694
+
695
+ Use Graphlient inside your RSpec tests in a Rails application or with `Rack::Test` against your actual application.
696
+
697
+ ```ruby
698
+ require 'spec_helper'
699
+
700
+ describe App do
701
+ include Rack::Test::Methods
702
+
703
+ def app
704
+ # ...
705
+ end
706
+
707
+ let(:client) do
708
+ Graphlient::Client.new('http://test-graphql.biz/graphql') do |client|
709
+ client.http do |h|
710
+ h.connection do |c|
711
+ c.adapter Faraday::Adapter::Rack, app
712
+ end
713
+ end
714
+ end
715
+ end
716
+
717
+ context 'an invoice' do
718
+ let(:result) do
719
+ client.query do
720
+ query do
721
+ invoice(id: 10) do
722
+ id
723
+ end
724
+ end
725
+ end
726
+ end
727
+
728
+ it 'can be retrieved' do
729
+ expect(result.data.invoice.id).to eq 10
730
+ end
731
+ end
732
+ end
733
+ ```
734
+
735
+ Alternately you can `stub_request` with Webmock.
736
+
737
+ ```ruby
738
+ describe App do
739
+ let(:url) { 'http://example.com/graphql' }
740
+ let(:client) { Graphlient::Client.new(url) }
741
+
742
+ before do
743
+ stub_request(:post, url).to_return(
744
+ status: 200,
745
+ body: DummySchema.execute(GraphQL::Introspection::INTROSPECTION_QUERY).to_json
746
+ )
747
+ end
748
+
749
+ it 'retrieves schema' do
750
+ expect(client.schema).to be_a Graphlient::Schema
751
+ end
752
+ end
753
+ ```
754
+
755
+ In order to stub the response to actual queries, [dump the schema into a JSON file](#schema-storing-and-loading-on-disk) and specify it via schema_path as follows.
756
+
757
+ ```ruby
758
+ describe App do
759
+ let(:url) { 'http://graph.biz/graphql' }
760
+ let(:client) { Graphlient::Client.new(url, schema_path: 'spec/support/fixtures/invoice_api.json') }
761
+ let(:query) do
762
+ <<~GRAPHQL
763
+ query{
764
+ invoice(id: 42) {
765
+ id
766
+ feeInCents
767
+ }
768
+ }
769
+ GRAPHQL
770
+ end
771
+ let(:json_response) do
772
+ {
773
+ 'data' => {
774
+ 'invoice' => {
775
+ 'id' => '42',
776
+ 'feeInCents' => 2000
777
+ }
778
+ }
779
+ }.to_json
780
+ end
781
+
782
+ before do
783
+ stub_request(:post, url).to_return(
784
+ status: 200,
785
+ body: json_response
786
+ )
787
+ end
788
+
789
+ it 'returns invoice fees' do
790
+ response = client.query(query)
791
+ expect(response.data).to be_truthy
792
+ expect(response.data.invoice.id).to eq('42')
793
+ expect(response.data.invoice.fee_in_cents).to eq(2000)
794
+ end
795
+ end
796
+ ```
797
+
798
+ ## License
799
+
800
+ MIT License, see [LICENSE](LICENSE)