gqli 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.rubocop_todo.yml +44 -0
- data/.travis.yml +13 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +154 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +284 -0
- data/Rakefile +33 -0
- data/coverage/.last_run.json +5 -0
- data/coverage/.resultset.json +512 -0
- data/coverage/.resultset.json.lock +0 -0
- data/coverage/assets/0.10.2/application.css +799 -0
- data/coverage/assets/0.10.2/application.js +1707 -0
- data/coverage/assets/0.10.2/colorbox/border.png +0 -0
- data/coverage/assets/0.10.2/colorbox/controls.png +0 -0
- data/coverage/assets/0.10.2/colorbox/loading.gif +0 -0
- data/coverage/assets/0.10.2/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.10.2/favicon_green.png +0 -0
- data/coverage/assets/0.10.2/favicon_red.png +0 -0
- data/coverage/assets/0.10.2/favicon_yellow.png +0 -0
- data/coverage/assets/0.10.2/loading.gif +0 -0
- data/coverage/assets/0.10.2/magnify.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/index.html +3255 -0
- data/doc/GQLi/Base.html +628 -0
- data/doc/GQLi/Client.html +830 -0
- data/doc/GQLi/DSL.html +421 -0
- data/doc/GQLi/Fragment.html +405 -0
- data/doc/GQLi/Introspection.html +837 -0
- data/doc/GQLi/Node.html +413 -0
- data/doc/GQLi/Query.html +390 -0
- data/doc/GQLi/Response.html +370 -0
- data/doc/GQLi.html +157 -0
- data/doc/_index.html +236 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +496 -0
- data/doc/file.CHANGELOG.html +76 -0
- data/doc/file.LICENSE.html +70 -0
- data/doc/file.README.html +365 -0
- data/doc/file_list.html +66 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +365 -0
- data/doc/js/app.js +292 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +355 -0
- data/doc/top-level-namespace.html +110 -0
- data/gqli.gemspec +37 -0
- data/lib/gqli/base.rb +53 -0
- data/lib/gqli/client.rb +59 -0
- data/lib/gqli/dsl.rb +37 -0
- data/lib/gqli/fragment.rb +25 -0
- data/lib/gqli/introspection.rb +215 -0
- data/lib/gqli/node.rb +48 -0
- data/lib/gqli/query.rb +29 -0
- data/lib/gqli/response.rb +15 -0
- data/lib/gqli/version.rb +7 -0
- data/lib/gqli.rb +6 -0
- data/spec/fixtures/vcr_cassettes/catCollection.yml +63 -0
- data/spec/fixtures/vcr_cassettes/client.yml +171 -0
- data/spec/fixtures/vcr_cassettes/validation_error.yml +62 -0
- data/spec/lib/gqli/client_spec.rb +90 -0
- data/spec/lib/gqli/dsl_spec.rb +205 -0
- data/spec/lib/gqli/introspection_spec.rb +125 -0
- data/spec/spec_helper.rb +25 -0
- data/usage.rb +100 -0
- data/usage_introspection.rb +63 -0
- data/usage_with_inlined_dsl.rb +69 -0
- metadata +387 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>
|
|
7
|
+
Top Level Namespace
|
|
8
|
+
|
|
9
|
+
— Documentation by YARD 0.9.16
|
|
10
|
+
|
|
11
|
+
</title>
|
|
12
|
+
|
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
|
|
14
|
+
|
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
|
|
16
|
+
|
|
17
|
+
<script type="text/javascript" charset="utf-8">
|
|
18
|
+
pathId = "";
|
|
19
|
+
relpath = '';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
|
24
|
+
|
|
25
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="nav_wrap">
|
|
31
|
+
<iframe id="nav" src="class_list.html?1"></iframe>
|
|
32
|
+
<div id="resizer"></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div id="main" tabindex="-1">
|
|
36
|
+
<div id="header">
|
|
37
|
+
<div id="menu">
|
|
38
|
+
|
|
39
|
+
<a href="_index.html">Index</a> »
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<span class="title">Top Level Namespace</span>
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div id="search">
|
|
47
|
+
|
|
48
|
+
<a class="full_list_link" id="class_list_link"
|
|
49
|
+
href="class_list.html">
|
|
50
|
+
|
|
51
|
+
<svg width="24" height="24">
|
|
52
|
+
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
|
|
53
|
+
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
|
|
54
|
+
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
|
|
55
|
+
</svg>
|
|
56
|
+
</a>
|
|
57
|
+
|
|
58
|
+
</div>
|
|
59
|
+
<div class="clear"></div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div id="content"><h1>Top Level Namespace
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
</h1>
|
|
67
|
+
<div class="box_info">
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h2>Defined Under Namespace</h2>
|
|
82
|
+
<p class="children">
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="GQLi.html" title="GQLi (module)">GQLi</a></span>
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div id="footer">
|
|
103
|
+
Generated on Mon Oct 15 13:10:42 2018 by
|
|
104
|
+
<a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
105
|
+
0.9.16 (ruby-2.5.1).
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
data/gqli.gemspec
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require File.expand_path('../lib/gqli/version', __FILE__)
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |gem|
|
|
4
|
+
gem.name = 'gqli'
|
|
5
|
+
gem.version = GQLi::VERSION
|
|
6
|
+
gem.summary = 'GraphQL client for humans'
|
|
7
|
+
gem.description = 'GraphQL client with simple interface, designed for developer happiness'
|
|
8
|
+
gem.license = 'MIT'
|
|
9
|
+
gem.authors = ['Contentful GmbH (David Litvak Bruno)']
|
|
10
|
+
gem.email = 'rubygems@contentful.com'
|
|
11
|
+
gem.homepage = 'https://github.com/contentful-labs/gqli.rb'
|
|
12
|
+
|
|
13
|
+
gem.files = Dir['{**/}{.*,*}'].select { |path| File.file?(path) && !path.start_with?('pkg') }
|
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
15
|
+
gem.test_files = gem.files.grep(%r{^spec/})
|
|
16
|
+
gem.require_paths = ['lib']
|
|
17
|
+
|
|
18
|
+
gem.add_dependency 'http', '> 0.8', '< 3.0'
|
|
19
|
+
gem.add_dependency 'hashie', '~> 3.0'
|
|
20
|
+
gem.add_dependency 'multi_json', '~> 1'
|
|
21
|
+
|
|
22
|
+
gem.add_development_dependency 'bundler', '~> 1.5'
|
|
23
|
+
gem.add_development_dependency 'rake', '< 11.0'
|
|
24
|
+
gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
|
|
25
|
+
|
|
26
|
+
gem.add_development_dependency 'guard'
|
|
27
|
+
gem.add_development_dependency 'guard-rspec'
|
|
28
|
+
gem.add_development_dependency 'guard-rubocop'
|
|
29
|
+
gem.add_development_dependency 'guard-yard'
|
|
30
|
+
gem.add_development_dependency 'rubocop', '~> 0.49.1'
|
|
31
|
+
gem.add_development_dependency 'rspec', '~> 3'
|
|
32
|
+
gem.add_development_dependency 'rr'
|
|
33
|
+
gem.add_development_dependency 'vcr'
|
|
34
|
+
gem.add_development_dependency 'simplecov'
|
|
35
|
+
gem.add_development_dependency 'webmock', '~> 1', '>= 1.17.3'
|
|
36
|
+
gem.add_development_dependency 'tins', '~> 1.6.0'
|
|
37
|
+
end
|
data/lib/gqli/base.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GQLi
|
|
4
|
+
# Base class for GraphQL type wrappers
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :__name, :__depth, :__nodes
|
|
7
|
+
|
|
8
|
+
def initialize(name = nil, depth = 0, &block)
|
|
9
|
+
@__name = name
|
|
10
|
+
@__depth = depth
|
|
11
|
+
@__nodes = []
|
|
12
|
+
instance_eval(&block) unless block.nil?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Inlines fragment nodes into current node
|
|
16
|
+
def ___(fragment)
|
|
17
|
+
@__nodes += __clone_nodes(fragment)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Adds type match node
|
|
21
|
+
def __on(type_name, &block)
|
|
22
|
+
require_relative './node'
|
|
23
|
+
@__nodes << Node.new("... on #{type_name}", [], __depth + 1, &block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def __clone_nodes(node_container)
|
|
29
|
+
require_relative './node'
|
|
30
|
+
__clone(node_container.__nodes).map do |n|
|
|
31
|
+
node = Node.new(n.__name, n.__params, __depth + 1)
|
|
32
|
+
node.instance_variable_set(
|
|
33
|
+
:@__nodes,
|
|
34
|
+
node.send(:__clone_nodes, n)
|
|
35
|
+
)
|
|
36
|
+
node
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def __clone(obj)
|
|
41
|
+
Marshal.load(Marshal.dump(obj))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def __params_from_args(args)
|
|
45
|
+
args.empty? ? {} : args[0]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def method_missing(name, *args, &block)
|
|
49
|
+
require_relative './node'
|
|
50
|
+
@__nodes << Node.new(name.to_s, __params_from_args(args), __depth + 1, &block)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/gqli/client.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative './response'
|
|
6
|
+
require_relative './introspection'
|
|
7
|
+
require_relative './version'
|
|
8
|
+
|
|
9
|
+
module GQLi
|
|
10
|
+
# GraphQL HTTP Client
|
|
11
|
+
class Client
|
|
12
|
+
attr_reader :url, :params, :headers, :validate_query, :schema
|
|
13
|
+
|
|
14
|
+
def initialize(url, params: {}, headers: {}, validate_query: true)
|
|
15
|
+
@url = url
|
|
16
|
+
@params = params
|
|
17
|
+
@headers = headers
|
|
18
|
+
@validate_query = validate_query
|
|
19
|
+
|
|
20
|
+
@schema = Introspection.new(self) if validate_query
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Executes a query
|
|
24
|
+
# If validations are enabled, will perform validation check before request.
|
|
25
|
+
def execute(query)
|
|
26
|
+
fail 'Validation Error: query is invalid - HTTP Request not sent' unless valid?(query)
|
|
27
|
+
|
|
28
|
+
execute!(query)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Executres a query
|
|
32
|
+
# Ignores validations
|
|
33
|
+
def execute!(query)
|
|
34
|
+
http_response = HTTP.headers(request_headers).post(@url, params: @params, json: { query: query.to_gql })
|
|
35
|
+
|
|
36
|
+
fail "Error: #{http_response.reason}\nBody: #{http_response.body}" if http_response.status >= 300
|
|
37
|
+
|
|
38
|
+
data = JSON.parse(http_response.to_s)['data']
|
|
39
|
+
|
|
40
|
+
Response.new(data, query)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Validates a query against the schema
|
|
44
|
+
def valid?(query)
|
|
45
|
+
return true unless validate_query
|
|
46
|
+
|
|
47
|
+
@schema.valid?(query)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def request_headers
|
|
53
|
+
{
|
|
54
|
+
accept: 'application/json',
|
|
55
|
+
user_agent: "gqli.rb/#{VERSION}; http.rb/#{HTTP::VERSION}"
|
|
56
|
+
}.merge(@headers)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/gqli/dsl.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './query'
|
|
4
|
+
require_relative './fragment'
|
|
5
|
+
|
|
6
|
+
module GQLi
|
|
7
|
+
# GraphQL-like DSL methods
|
|
8
|
+
module DSL
|
|
9
|
+
# Creates a Query object
|
|
10
|
+
#
|
|
11
|
+
# Can be used at a class level
|
|
12
|
+
def self.query(name = nil, &block)
|
|
13
|
+
Query.new(name, &block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Creates a Fragment object
|
|
17
|
+
#
|
|
18
|
+
# Can be used at a class level
|
|
19
|
+
def self.fragment(name, on, &block)
|
|
20
|
+
Fragment.new(name, on, &block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Creates a Query object
|
|
24
|
+
#
|
|
25
|
+
# Can be used at an instance level
|
|
26
|
+
def query(name = nil, &block)
|
|
27
|
+
Query.new(name, &block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Creates a Fragment object
|
|
31
|
+
#
|
|
32
|
+
# Can be used at an instance level
|
|
33
|
+
def fragment(name, on, &block)
|
|
34
|
+
Fragment.new(name, on, &block)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './base'
|
|
4
|
+
require_relative './node'
|
|
5
|
+
|
|
6
|
+
module GQLi
|
|
7
|
+
# Fragment wrapper
|
|
8
|
+
class Fragment < Base
|
|
9
|
+
attr_reader :__on_type
|
|
10
|
+
|
|
11
|
+
def initialize(name, on, &block)
|
|
12
|
+
super(name, 0, &block)
|
|
13
|
+
@__on_type = on
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Serializes to a GraphQL string
|
|
17
|
+
def to_gql
|
|
18
|
+
<<~GQL
|
|
19
|
+
fragment #{__name} on #{__on_type} {
|
|
20
|
+
#{__nodes.map(&:to_gql).join("\n")}
|
|
21
|
+
}
|
|
22
|
+
GQL
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './dsl'
|
|
4
|
+
|
|
5
|
+
module GQLi
|
|
6
|
+
# Introspection schema and validator
|
|
7
|
+
class Introspection
|
|
8
|
+
extend DSL
|
|
9
|
+
|
|
10
|
+
# Specific type kind introspection fragment
|
|
11
|
+
TypeRef = fragment('TypeRef', '__Type') {
|
|
12
|
+
kind
|
|
13
|
+
name
|
|
14
|
+
ofType {
|
|
15
|
+
kind
|
|
16
|
+
name
|
|
17
|
+
ofType {
|
|
18
|
+
kind
|
|
19
|
+
name
|
|
20
|
+
ofType {
|
|
21
|
+
kind
|
|
22
|
+
name
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Input value introspection fragment
|
|
29
|
+
InputValue = fragment('InputValue', '__InputValue') {
|
|
30
|
+
name
|
|
31
|
+
description
|
|
32
|
+
type { ___ TypeRef }
|
|
33
|
+
defaultValue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Type introspection fragment
|
|
37
|
+
FullType = fragment('FullType', '__Type') {
|
|
38
|
+
kind
|
|
39
|
+
name
|
|
40
|
+
description
|
|
41
|
+
fields(includeDeprecated: true) {
|
|
42
|
+
name
|
|
43
|
+
description
|
|
44
|
+
args { ___ InputValue }
|
|
45
|
+
type { ___ TypeRef }
|
|
46
|
+
isDeprecated
|
|
47
|
+
deprecationReason
|
|
48
|
+
}
|
|
49
|
+
inputFields { ___ InputValue }
|
|
50
|
+
interfaces { ___ TypeRef }
|
|
51
|
+
enumValues(includeDeprecated: true) {
|
|
52
|
+
name
|
|
53
|
+
description
|
|
54
|
+
isDeprecated
|
|
55
|
+
deprecationReason
|
|
56
|
+
}
|
|
57
|
+
possibleTypes { ___ TypeRef }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Query for fetching the complete schema
|
|
61
|
+
IntrospectionQuery = query {
|
|
62
|
+
__schema {
|
|
63
|
+
queryType { name }
|
|
64
|
+
mutationType { name }
|
|
65
|
+
subscriptionType { name }
|
|
66
|
+
types { ___ FullType }
|
|
67
|
+
directives {
|
|
68
|
+
name
|
|
69
|
+
description
|
|
70
|
+
args { ___ InputValue }
|
|
71
|
+
onOperation
|
|
72
|
+
onFragment
|
|
73
|
+
onField
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
attr_reader :schema, :query_type, :mutation_type, :subscription_type, :types
|
|
79
|
+
|
|
80
|
+
def initialize(client)
|
|
81
|
+
@schema = client.execute!(IntrospectionQuery).data.__schema
|
|
82
|
+
@query_type = schema.queryType
|
|
83
|
+
@mutation_type = schema.mutationType
|
|
84
|
+
@subscription_type = schema.subscriptionType
|
|
85
|
+
@types = schema.types
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns wether the query is valid or not
|
|
89
|
+
def valid?(query)
|
|
90
|
+
return false unless query.is_a?(Query)
|
|
91
|
+
|
|
92
|
+
query_type = types.find { |t| t.name.casecmp('query').zero? }
|
|
93
|
+
query.__nodes.each do |node|
|
|
94
|
+
return false unless valid_node?(query_type, node)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def valid_node?(parent_type, node)
|
|
103
|
+
return true if parent_type.kind == 'SCALAR'
|
|
104
|
+
|
|
105
|
+
return valid_match_node?(parent_type, node) if node.__name.start_with?('... on')
|
|
106
|
+
|
|
107
|
+
node_type = parent_type.fetch('fields', []).find { |f| f.name == node.__name }
|
|
108
|
+
return false if node_type.nil?
|
|
109
|
+
|
|
110
|
+
return false unless valid_params?(node_type, node)
|
|
111
|
+
|
|
112
|
+
resolved_node_type = type_for(node_type)
|
|
113
|
+
return false if resolved_node_type.nil?
|
|
114
|
+
|
|
115
|
+
return false unless valid_nesting_node?(resolved_node_type, node)
|
|
116
|
+
|
|
117
|
+
node.__nodes.all? { |n| valid_node?(resolved_node_type, n) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def valid_match_node?(parent_type, node)
|
|
121
|
+
return true if parent_type.fetch('possibleTypes', []).find { |t| t.name == node.__name.gsub('... on ', '') }
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def valid_params?(node_type, node)
|
|
126
|
+
node.__params.each do |param, value|
|
|
127
|
+
arg = node_type.fetch('args', []).find { |a| a.name == param.to_s }
|
|
128
|
+
return false if arg.nil?
|
|
129
|
+
|
|
130
|
+
arg_type = type_for(arg)
|
|
131
|
+
return false if arg_type.nil?
|
|
132
|
+
|
|
133
|
+
return false unless valid_value_for_type?(arg_type, value)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def valid_nesting_node?(node_type, node)
|
|
140
|
+
return false unless valid_object_node?(node_type, node)
|
|
141
|
+
return false unless valid_array_node?(node_type, node)
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def valid_object_node?(node_type, node)
|
|
146
|
+
return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def valid_array_node?(node_type, node)
|
|
151
|
+
return false if %w[OBJECT INTERFACE].include?(node_type.kind) && node.__nodes.empty?
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def valid_value_for_type?(arg_type, value)
|
|
156
|
+
case value
|
|
157
|
+
when ::String
|
|
158
|
+
return false unless arg_type.name == 'String' || arg_type.name == 'ID'
|
|
159
|
+
when ::Integer
|
|
160
|
+
return false unless arg_type.name == 'Int'
|
|
161
|
+
when ::Float
|
|
162
|
+
return false unless arg_type.name == 'Float'
|
|
163
|
+
when ::Hash
|
|
164
|
+
return valid_hash_value?(arg_type, value)
|
|
165
|
+
when true, false
|
|
166
|
+
return false unless arg_type.name == 'Boolean'
|
|
167
|
+
else
|
|
168
|
+
return false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def valid_hash_value?(arg_type, value)
|
|
175
|
+
return false unless arg_type.kind == 'INPUT_OBJECT'
|
|
176
|
+
|
|
177
|
+
type = types.find { |f| f.name == arg_type.name }
|
|
178
|
+
return false if type.nil?
|
|
179
|
+
|
|
180
|
+
value.each do |k, v|
|
|
181
|
+
input_field = type.fetch('inputFields', []).find { |f| f.name == k.to_s }
|
|
182
|
+
return false if input_field.nil?
|
|
183
|
+
|
|
184
|
+
input_field_type = type_for(input_field)
|
|
185
|
+
return false if input_field_type.nil?
|
|
186
|
+
|
|
187
|
+
return false unless valid_value_for_type?(input_field_type, v)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def type_for(field_type)
|
|
192
|
+
type = case field_type.type.kind
|
|
193
|
+
when 'NON_NULL'
|
|
194
|
+
non_null_type(field_type.type.ofType)
|
|
195
|
+
when 'LIST'
|
|
196
|
+
field_type.type.ofType
|
|
197
|
+
when 'OBJECT', 'INTERFACE', 'INPUT_OBJECT'
|
|
198
|
+
field_type.type
|
|
199
|
+
when 'SCALAR'
|
|
200
|
+
field_type.type
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
types.find { |t| t.name == type.name }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def non_null_type(non_null)
|
|
207
|
+
case non_null.kind
|
|
208
|
+
when 'LIST'
|
|
209
|
+
non_null.ofType
|
|
210
|
+
else
|
|
211
|
+
non_null
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
data/lib/gqli/node.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './base'
|
|
4
|
+
|
|
5
|
+
module GQLi
|
|
6
|
+
# Node wrapper
|
|
7
|
+
class Node < Base
|
|
8
|
+
attr_reader :__params
|
|
9
|
+
|
|
10
|
+
def initialize(name, params = {}, depth = 1, &block)
|
|
11
|
+
super(name, depth, &block)
|
|
12
|
+
@__params = params
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Serializes to a GraphQL string
|
|
16
|
+
def to_gql
|
|
17
|
+
result = ' ' * __depth + __name
|
|
18
|
+
result += '(' + __params_to_s(__params, true) + ')' unless __params.empty?
|
|
19
|
+
unless __nodes.empty?
|
|
20
|
+
result += " {\n"
|
|
21
|
+
result += __nodes.map(&:to_gql).join("\n")
|
|
22
|
+
result += "\n#{' ' * __depth}}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def __params_to_s(params, initial = false)
|
|
31
|
+
case params
|
|
32
|
+
when ::Hash
|
|
33
|
+
result = params.map do |k, v|
|
|
34
|
+
"#{k}: #{__params_to_s(v)}"
|
|
35
|
+
end.join(', ')
|
|
36
|
+
|
|
37
|
+
return result if initial
|
|
38
|
+
"{#{result}}"
|
|
39
|
+
when ::Array
|
|
40
|
+
"[#{params.map { |p| __params_to_s(p) }.join(', ')}]"
|
|
41
|
+
when ::String
|
|
42
|
+
"\"#{params}\""
|
|
43
|
+
else
|
|
44
|
+
params.to_s
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/gqli/query.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './base'
|
|
4
|
+
|
|
5
|
+
module GQLi
|
|
6
|
+
# Query node
|
|
7
|
+
class Query < Base
|
|
8
|
+
# Serializes to a GraphQL string
|
|
9
|
+
def to_gql
|
|
10
|
+
result = <<~GQL
|
|
11
|
+
query #{__name ? __name + ' ' : ''}{
|
|
12
|
+
#{__nodes.map(&:to_gql).join("\n")}
|
|
13
|
+
}
|
|
14
|
+
GQL
|
|
15
|
+
|
|
16
|
+
result.lstrip
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Delegates itself to the client to be executed
|
|
20
|
+
def __execute(client)
|
|
21
|
+
client.execute(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Serializes to a GraphQL string
|
|
25
|
+
def to_s
|
|
26
|
+
to_gql
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/gqli/version.rb
ADDED