graphql-client 0.0.5 → 0.0.6
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 +4 -4
- data/lib/graphql/client.rb +93 -7
- data/lib/graphql/client/const_proxy.rb +29 -0
- data/lib/graphql/client/query_result.rb +52 -8
- data/lib/graphql/language/mutator.rb +45 -0
- data/lib/graphql/language/operation_slice.rb +31 -0
- data/lib/graphql/relay/parser.rb +27 -0
- metadata +7 -16
- data/lib/graphql/client/document.rb +0 -47
- data/lib/graphql/client/fragment.rb +0 -32
- data/lib/graphql/client/node.rb +0 -36
- data/lib/graphql/client/query.rb +0 -30
- data/lib/graphql/language/nodes/inject_selection_ext.rb +0 -44
- data/lib/graphql/language/nodes/query_result_class_ext.rb +0 -70
- data/lib/graphql/language/nodes/replace_fragment_spread_ext.rb +0 -44
- data/lib/graphql/language/nodes/selection_ext.rb +0 -37
- data/lib/graphql/language/nodes/validate_ext.rb +0 -45
- data/lib/graphql/relay/node_query.rb +0 -19
- data/lib/graphql/schema/json_loader.rb +0 -101
- data/lib/graphql/schema_load_json.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 852686ecf24561871545329aeb4868baaccb99de
|
4
|
+
data.tar.gz: 2bd0845ad82f0ba555497e0a46c315a9d8a6fa0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58701e7031146155bcf9584bf61cdbee25f6ac9bf61121eccd5e915970624d2755aef071f18c781d38fcbf3ca1878fccff4f22d33b7dbe3c537598a597658f23
|
7
|
+
data.tar.gz: 8dfe674b8d2c39233fcdfd533e8ea516c010e0cecb7be38fb01c2128f76dc466e61e7dd334e0dff36e810465d9e9f6064d7c5f9969de9e65e081fa0dba0bc9ea
|
data/lib/graphql/client.rb
CHANGED
@@ -1,14 +1,100 @@
|
|
1
|
+
require "active_support/inflector"
|
1
2
|
require "graphql"
|
2
|
-
require "graphql/client/
|
3
|
-
require "graphql/client/fragment"
|
4
|
-
require "graphql/client/node"
|
3
|
+
require "graphql/client/const_proxy"
|
5
4
|
require "graphql/client/query_result"
|
6
|
-
require "graphql/
|
5
|
+
require "graphql/language/mutator"
|
6
|
+
require "graphql/language/nodes/deep_freeze_ext"
|
7
|
+
require "graphql/language/operation_slice"
|
8
|
+
require "graphql/relay/parser"
|
7
9
|
|
8
10
|
module GraphQL
|
9
|
-
|
10
|
-
class
|
11
|
-
|
11
|
+
class Client
|
12
|
+
class Error < StandardError; end
|
13
|
+
class ValidationError < Error; end
|
14
|
+
|
15
|
+
attr_reader :schema
|
16
|
+
|
17
|
+
def initialize(schema:)
|
18
|
+
@schema = schema
|
19
|
+
@definitions = []
|
20
|
+
end
|
21
|
+
|
22
|
+
class Definition
|
23
|
+
def initialize(name:, client:, source:)
|
24
|
+
@name = name
|
25
|
+
@client = client
|
26
|
+
@_nodes = @client._parse(@name, source)
|
27
|
+
@query_result = GraphQL::Client::QueryResult.wrap(@_nodes.first, name: name)
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :_nodes
|
31
|
+
|
32
|
+
def operation_name
|
33
|
+
if op = @_nodes.find { |d| d.is_a?(GraphQL::Language::Nodes::OperationDefinition) }
|
34
|
+
op.name
|
35
|
+
else
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def document
|
41
|
+
@document ||= Language::OperationSlice.slice(@client.document, operation_name).deep_freeze
|
42
|
+
end
|
43
|
+
|
44
|
+
def new(*args)
|
45
|
+
@query_result.new(*args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(str)
|
50
|
+
definition = ConstProxy.new { |name| Definition.new(client: self, name: name, source: str) }
|
51
|
+
@definitions << definition
|
52
|
+
definition
|
53
|
+
end
|
54
|
+
|
55
|
+
def _parse(name, str)
|
56
|
+
str = str.strip
|
57
|
+
|
58
|
+
str = str.gsub(/\.\.\.([a-zA-Z0-9_]+(::[a-zA-Z0-9_]+)+)(\.([a-zA-Z0-9_]+))?/) { |m|
|
59
|
+
const_name, fragment_name = $1, $4
|
60
|
+
nodes = ActiveSupport::Inflector.constantize(const_name)._nodes
|
61
|
+
|
62
|
+
fragment_name = fragment_name ?
|
63
|
+
nodes.find { |n| n.name.end_with?(fragment_name) }.name : # XXX
|
64
|
+
nodes.first.name
|
65
|
+
|
66
|
+
"...#{fragment_name}"
|
67
|
+
}
|
68
|
+
|
69
|
+
doc = GraphQL::Relay::Parser.parse(str)
|
70
|
+
|
71
|
+
mutator = GraphQL::Language::Mutator.new(doc)
|
72
|
+
|
73
|
+
aliases = {}
|
74
|
+
doc.definitions.each do |definition|
|
75
|
+
aliases[definition.name] = (name.split("::") << definition.name).compact.join("__")
|
76
|
+
end
|
77
|
+
mutator.rename_definitions(aliases)
|
78
|
+
|
79
|
+
# TODO: Make this __typename injection optional
|
80
|
+
mutator.prepend_selection(GraphQL::Language::Nodes::Field.new(name: "__typename").deep_freeze)
|
81
|
+
|
82
|
+
doc.definitions.map(&:deep_freeze)
|
83
|
+
end
|
84
|
+
|
85
|
+
def document
|
86
|
+
GraphQL::Language::Nodes::Document.new(definitions: @definitions.flat_map(&:_nodes)).deep_freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
def validate!
|
90
|
+
validator = StaticValidation::Validator.new(schema: @schema)
|
91
|
+
query = Query.new(@schema, document: document)
|
92
|
+
|
93
|
+
validator.validate(query).fetch(:errors).each do |error|
|
94
|
+
raise ValidationError, error["message"]
|
95
|
+
end
|
96
|
+
|
97
|
+
nil
|
12
98
|
end
|
13
99
|
end
|
14
100
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module GraphQL
|
2
|
+
class Client
|
3
|
+
# Internal: Hack to track the constant name an object is assigned to.
|
4
|
+
#
|
5
|
+
# FooConstant = ConstProxy.new { |name|
|
6
|
+
# name # "FooConstant"
|
7
|
+
# }
|
8
|
+
#
|
9
|
+
module ConstProxy
|
10
|
+
def self.new(&initializer)
|
11
|
+
raise ArgumentError, "initializer required" unless block_given?
|
12
|
+
|
13
|
+
Module.new do
|
14
|
+
extend ConstProxy
|
15
|
+
@initializer = initializer
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
super || raise(RuntimeError, "expected object to be assigned to a constant")
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(*args, &block)
|
24
|
+
@target ||= @initializer.call(self.name)
|
25
|
+
@target.send(*args, &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,15 +1,38 @@
|
|
1
|
-
require "graphql"
|
2
1
|
require "active_support/inflector"
|
2
|
+
require "graphql"
|
3
|
+
require "set"
|
3
4
|
|
4
5
|
module GraphQL
|
5
|
-
|
6
|
+
class Client
|
6
7
|
class QueryResult
|
7
|
-
|
8
|
-
|
8
|
+
# Internal: Get QueryResult class for result of query.
|
9
|
+
#
|
10
|
+
# Returns subclass of QueryResult or nil.
|
11
|
+
def self.wrap(node, name: nil)
|
12
|
+
fields = {}
|
13
|
+
|
14
|
+
node.selections.each do |selection|
|
15
|
+
case selection
|
16
|
+
when Language::Nodes::FragmentSpread
|
17
|
+
when Language::Nodes::Field
|
18
|
+
field_name = selection.alias || selection.name
|
19
|
+
field_klass = selection.selections.any? ? wrap(selection, name: "#{name}.#{field_name}") : nil
|
20
|
+
fields[field_name] ? fields[field_name] |= field_klass : fields[field_name] = field_klass
|
21
|
+
when Language::Nodes::InlineFragment
|
22
|
+
wrap(selection, name: name).fields.each do |fragment_name, klass|
|
23
|
+
fields[fragment_name.to_s] ? fields[fragment_name.to_s] |= klass : fields[fragment_name.to_s] = klass
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
define(name: name, source_node: node, fields: fields)
|
9
29
|
end
|
10
30
|
|
11
|
-
|
31
|
+
# Internal
|
32
|
+
def self.define(name:, source_node:, fields: {})
|
12
33
|
Class.new(self) do
|
34
|
+
@name = name
|
35
|
+
@source_node = source_node
|
13
36
|
@fields = {}
|
14
37
|
|
15
38
|
fields.each do |field, type|
|
@@ -62,8 +85,20 @@ module GraphQL
|
|
62
85
|
end
|
63
86
|
end
|
64
87
|
|
88
|
+
def self.source_node
|
89
|
+
@source_node
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.fields
|
93
|
+
@fields
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.name
|
97
|
+
@name || super || GraphQL::Client::QueryResult.name
|
98
|
+
end
|
99
|
+
|
65
100
|
def self.inspect
|
66
|
-
"
|
101
|
+
"#<#{self.name} fields=#{@fields.keys.inspect}>"
|
67
102
|
end
|
68
103
|
|
69
104
|
def self.cast(obj)
|
@@ -71,6 +106,14 @@ module GraphQL
|
|
71
106
|
when Hash
|
72
107
|
new(obj)
|
73
108
|
when QueryResult
|
109
|
+
spreads = Set.new(obj.class.source_node.selections.select { |s| s.is_a?(GraphQL::Language::Nodes::FragmentSpread) }.map(&:name))
|
110
|
+
|
111
|
+
if !spreads.include?(self.source_node.name)
|
112
|
+
message = "couldn't cast #{obj.inspect} to #{self.inspect}\n\n"
|
113
|
+
suggestion = "\n ...#{name || "YourFragment"} # SUGGESTION"
|
114
|
+
message << GraphQL::Language::Generation.generate(obj.class.source_node).sub(/\n}$/, "#{suggestion}\n}")
|
115
|
+
raise TypeError, message
|
116
|
+
end
|
74
117
|
cast(obj.to_h)
|
75
118
|
when Array
|
76
119
|
obj.map { |e| cast(e) }
|
@@ -99,7 +142,8 @@ module GraphQL
|
|
99
142
|
new_fields[name] = value
|
100
143
|
end
|
101
144
|
end
|
102
|
-
|
145
|
+
# TODO: Picking first source node seems error prone
|
146
|
+
define(name: self.name, source_node: self.source_node, fields: new_fields)
|
103
147
|
end
|
104
148
|
|
105
149
|
attr_reader :data
|
@@ -107,7 +151,7 @@ module GraphQL
|
|
107
151
|
|
108
152
|
def inspect
|
109
153
|
ivars = (self.class.fields.keys - [:__typename]).map { |sym| "#{sym}=#{instance_variable_get("@#{sym}").inspect}" }
|
110
|
-
buf = "
|
154
|
+
buf = "#<#{self.class.name}"
|
111
155
|
buf << " " << @__typename if @__typename
|
112
156
|
buf << " " << ivars.join(" ") if ivars.any?
|
113
157
|
buf << ">"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "graphql"
|
2
|
+
require "set"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Language
|
6
|
+
class Mutator
|
7
|
+
def initialize(document)
|
8
|
+
@document = document
|
9
|
+
end
|
10
|
+
|
11
|
+
def rename_definitions(definitions)
|
12
|
+
rename_node = -> (node, parent) {
|
13
|
+
node.name = definitions.fetch(node.name, node.name)
|
14
|
+
}
|
15
|
+
|
16
|
+
visitor = Visitor.new(@document)
|
17
|
+
visitor[Nodes::FragmentDefinition].leave << rename_node
|
18
|
+
visitor[Nodes::OperationDefinition].leave << rename_node
|
19
|
+
visitor[Nodes::FragmentSpread].leave << rename_node
|
20
|
+
visitor.visit
|
21
|
+
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def prepend_selection(selection)
|
26
|
+
on_selections = -> (node, parent) {
|
27
|
+
return if !node.selections.any?
|
28
|
+
# TODO: Simplify if AbstractNode#eql? is implemented
|
29
|
+
existing_selections = Set.new(node.selections.map { |s| s.respond_to?(:name) ? s.name : nil }.compact)
|
30
|
+
selections_to_prepend = [selection].reject { |s| existing_selections.include?(s.name) }
|
31
|
+
node.selections = selections_to_prepend + node.selections
|
32
|
+
}
|
33
|
+
|
34
|
+
visitor = Visitor.new(@document)
|
35
|
+
visitor[Nodes::Field].leave << on_selections
|
36
|
+
visitor[Nodes::FragmentDefinition].leave << on_selections
|
37
|
+
visitor[Nodes::InlineFragment].leave << on_selections
|
38
|
+
visitor[Nodes::OperationDefinition].leave << on_selections
|
39
|
+
visitor.visit
|
40
|
+
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "graphql"
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Language
|
5
|
+
module OperationSlice
|
6
|
+
# Public: Return's minimal document to represent operation.
|
7
|
+
#
|
8
|
+
# Find's target operation and any fragment dependencies and returns a
|
9
|
+
# new document with just those definitions.
|
10
|
+
#
|
11
|
+
# document - The Nodes::Document to find definitions.
|
12
|
+
# operation_name - The String name of Nodes::OperationDefinition
|
13
|
+
#
|
14
|
+
# Returns new Nodes::Document.
|
15
|
+
def self.slice(document, operation_name)
|
16
|
+
definitions = []
|
17
|
+
definitions << document.definitions.find { |d| d.name == operation_name }
|
18
|
+
|
19
|
+
visitor = Visitor.new(document)
|
20
|
+
visitor[Nodes::FragmentSpread] << -> (node, parent) {
|
21
|
+
if fragment = document.definitions.find { |d| d.name == node.name }
|
22
|
+
definitions << fragment
|
23
|
+
end
|
24
|
+
}
|
25
|
+
visitor.visit
|
26
|
+
|
27
|
+
Nodes::Document.new(definitions: definitions.uniq)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "graphql"
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Relay
|
5
|
+
module Parser
|
6
|
+
ANONYMOUS_SENTINEL = "__anonymous__".freeze
|
7
|
+
|
8
|
+
# Public: Extended GraphQL.parse that supports Relay style anonymous
|
9
|
+
# fragments.
|
10
|
+
#
|
11
|
+
# TODO: See about getting support for this upstreamed to the graphql-ruby
|
12
|
+
# gem.
|
13
|
+
#
|
14
|
+
# str - A GraphQL String
|
15
|
+
#
|
16
|
+
# Returns a GraphQL::Language::Nodes::Document.
|
17
|
+
def self.parse(str)
|
18
|
+
str = str.sub(/fragment on /, "fragment #{ANONYMOUS_SENTINEL} on ")
|
19
|
+
document = GraphQL.parse(str)
|
20
|
+
document.definitions.each do |node|
|
21
|
+
node.name = nil if node.name == ANONYMOUS_SENTINEL
|
22
|
+
end
|
23
|
+
document
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -74,20 +74,12 @@ extra_rdoc_files: []
|
|
74
74
|
files:
|
75
75
|
- LICENSE
|
76
76
|
- lib/graphql/client.rb
|
77
|
-
- lib/graphql/client/
|
78
|
-
- lib/graphql/client/fragment.rb
|
79
|
-
- lib/graphql/client/node.rb
|
80
|
-
- lib/graphql/client/query.rb
|
77
|
+
- lib/graphql/client/const_proxy.rb
|
81
78
|
- lib/graphql/client/query_result.rb
|
79
|
+
- lib/graphql/language/mutator.rb
|
82
80
|
- lib/graphql/language/nodes/deep_freeze_ext.rb
|
83
|
-
- lib/graphql/language/
|
84
|
-
- lib/graphql/
|
85
|
-
- lib/graphql/language/nodes/replace_fragment_spread_ext.rb
|
86
|
-
- lib/graphql/language/nodes/selection_ext.rb
|
87
|
-
- lib/graphql/language/nodes/validate_ext.rb
|
88
|
-
- lib/graphql/relay/node_query.rb
|
89
|
-
- lib/graphql/schema/json_loader.rb
|
90
|
-
- lib/graphql/schema_load_json.rb
|
81
|
+
- lib/graphql/language/operation_slice.rb
|
82
|
+
- lib/graphql/relay/parser.rb
|
91
83
|
homepage:
|
92
84
|
licenses:
|
93
85
|
- MIT
|
@@ -108,9 +100,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
100
|
version: '0'
|
109
101
|
requirements: []
|
110
102
|
rubyforge_project:
|
111
|
-
rubygems_version: 2.5.1
|
103
|
+
rubygems_version: 2.4.5.1
|
112
104
|
signing_key:
|
113
105
|
specification_version: 4
|
114
106
|
summary: "???"
|
115
107
|
test_files: []
|
116
|
-
has_rdoc:
|
@@ -1,47 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "graphql/client/fragment"
|
3
|
-
require "graphql/client/node"
|
4
|
-
require "graphql/client/query"
|
5
|
-
require "graphql/language/nodes/deep_freeze_ext"
|
6
|
-
require "graphql/language/nodes/inject_selection_ext"
|
7
|
-
require "graphql/language/nodes/replace_fragment_spread_ext"
|
8
|
-
require "graphql/language/nodes/validate_ext"
|
9
|
-
|
10
|
-
module GraphQL
|
11
|
-
module Client
|
12
|
-
class Document < Node
|
13
|
-
def self.parse(str, schema: GraphQL::Client.schema)
|
14
|
-
str = str.strip
|
15
|
-
str, fragments = scan_interpolated_fragments(str)
|
16
|
-
|
17
|
-
document = GraphQL.parse(str)
|
18
|
-
document = document.inject_selection(GraphQL::Language::Nodes::Field.new(name: "__typename"))
|
19
|
-
|
20
|
-
document.definitions.each do |definition|
|
21
|
-
fragments[definition.name.to_sym] = definition if definition.is_a?(GraphQL::Language::Nodes::FragmentDefinition)
|
22
|
-
end
|
23
|
-
|
24
|
-
document = document.replace_fragment_spread(fragments)
|
25
|
-
|
26
|
-
document.definitions.inject({}) do |doc, definition|
|
27
|
-
name = definition.name.to_sym
|
28
|
-
|
29
|
-
case definition
|
30
|
-
when GraphQL::Language::Nodes::OperationDefinition
|
31
|
-
query = GraphQL::Client::Query.new(definition.deep_freeze, fragments.values).freeze
|
32
|
-
query.node.validate!(schema: schema) if schema
|
33
|
-
doc[name] = query
|
34
|
-
|
35
|
-
when GraphQL::Language::Nodes::FragmentDefinition
|
36
|
-
definition = GraphQL::Language::Nodes::InlineFragment.new(type: definition.type, directives: definition.directives, selections: definition.selections)
|
37
|
-
fragment = GraphQL::Client::Fragment.new(definition.deep_freeze, fragments.values).freeze
|
38
|
-
fragment.node.validate!(schema: schema) if schema
|
39
|
-
doc[name] = fragment
|
40
|
-
end
|
41
|
-
|
42
|
-
doc
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "graphql/client/node"
|
3
|
-
require "graphql/language/nodes/deep_freeze_ext"
|
4
|
-
require "graphql/language/nodes/inject_selection_ext"
|
5
|
-
require "graphql/language/nodes/replace_fragment_spread_ext"
|
6
|
-
require "graphql/language/nodes/validate_ext"
|
7
|
-
|
8
|
-
module GraphQL
|
9
|
-
module Client
|
10
|
-
class Fragment < Node
|
11
|
-
def self.parse(str, schema: GraphQL::Client.schema)
|
12
|
-
str = str.strip
|
13
|
-
str, fragments = scan_interpolated_fragments(str)
|
14
|
-
|
15
|
-
if str.start_with?("fragment")
|
16
|
-
str = str.sub(/^fragment on /, "fragment __anonymous__ on ")
|
17
|
-
doc = GraphQL.parse(str)
|
18
|
-
doc = doc.inject_selection(GraphQL::Language::Nodes::Field.new(name: "__typename"))
|
19
|
-
doc = doc.replace_fragment_spread(fragments)
|
20
|
-
fragment = doc.definitions.first
|
21
|
-
node = GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, directives: fragment.directives, selections: fragment.selections)
|
22
|
-
else
|
23
|
-
raise ArgumentError, "expected string to be a fragment:\n#{str}"
|
24
|
-
end
|
25
|
-
|
26
|
-
fragment = new(node.deep_freeze, fragments.values).freeze
|
27
|
-
fragment.node.validate!(schema: schema) if schema
|
28
|
-
fragment
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
data/lib/graphql/client/node.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
require "active_support/inflector"
|
2
|
-
require "graphql"
|
3
|
-
require "graphql/language/nodes/query_result_class_ext"
|
4
|
-
|
5
|
-
module GraphQL
|
6
|
-
module Client
|
7
|
-
class Node
|
8
|
-
def self.scan_interpolated_fragments(str)
|
9
|
-
fragments, index = {}, 1
|
10
|
-
str = str.gsub(/\.\.\.([a-zA-Z0-9_]+(::[a-zA-Z0-9_]+)+)/) { |m|
|
11
|
-
index += 1
|
12
|
-
name = "__fragment#{index}__"
|
13
|
-
fragments[name.to_sym] = ActiveSupport::Inflector.constantize($1).node
|
14
|
-
"...#{name}"
|
15
|
-
}
|
16
|
-
return str, fragments
|
17
|
-
end
|
18
|
-
|
19
|
-
attr_reader :node
|
20
|
-
|
21
|
-
attr_reader :fragments
|
22
|
-
|
23
|
-
attr_reader :type
|
24
|
-
|
25
|
-
def initialize(node, fragments)
|
26
|
-
@node = node
|
27
|
-
@fragments = fragments
|
28
|
-
@type = node.query_result_class(shadow: fragments)
|
29
|
-
end
|
30
|
-
|
31
|
-
def new(*args)
|
32
|
-
type.new(*args)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
data/lib/graphql/client/query.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "graphql/client/node"
|
3
|
-
require "graphql/language/nodes/deep_freeze_ext"
|
4
|
-
require "graphql/language/nodes/inject_selection_ext"
|
5
|
-
require "graphql/language/nodes/replace_fragment_spread_ext"
|
6
|
-
require "graphql/language/nodes/validate_ext"
|
7
|
-
|
8
|
-
module GraphQL
|
9
|
-
module Client
|
10
|
-
class Query < Node
|
11
|
-
def self.parse(str, schema: GraphQL::Client.schema)
|
12
|
-
str = str.strip
|
13
|
-
str, fragments = scan_interpolated_fragments(str)
|
14
|
-
|
15
|
-
if str.start_with?("query")
|
16
|
-
doc = GraphQL.parse(str)
|
17
|
-
doc = doc.inject_selection(GraphQL::Language::Nodes::Field.new(name: "__typename"))
|
18
|
-
doc = doc.replace_fragment_spread(fragments)
|
19
|
-
node = doc.definitions.first
|
20
|
-
else
|
21
|
-
raise ArgumentError, "expected string to be a query:\n#{str}"
|
22
|
-
end
|
23
|
-
|
24
|
-
query = new(node.deep_freeze, fragments.values).freeze
|
25
|
-
query.node.validate!(schema: schema) if schema
|
26
|
-
query
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "graphql/language/nodes/selection_ext"
|
3
|
-
|
4
|
-
module GraphQL
|
5
|
-
module Language
|
6
|
-
module Nodes
|
7
|
-
module Selections
|
8
|
-
def inject_selection(*selections)
|
9
|
-
other = self.dup
|
10
|
-
other.selections = selections + self.selections.map do |selection|
|
11
|
-
case selection
|
12
|
-
when Selections
|
13
|
-
selection.inject_selection(*selections)
|
14
|
-
else
|
15
|
-
selection
|
16
|
-
end
|
17
|
-
end
|
18
|
-
other
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class Document < AbstractNode
|
23
|
-
def inject_selection(*args)
|
24
|
-
other = self.dup
|
25
|
-
other.definitions = self.definitions.map do |definition|
|
26
|
-
case definition
|
27
|
-
when Selections
|
28
|
-
definition.inject_selection(*args)
|
29
|
-
else
|
30
|
-
definition
|
31
|
-
end
|
32
|
-
end
|
33
|
-
other
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
class Field < AbstractNode
|
38
|
-
def inject_selection(*selections)
|
39
|
-
self.selections.any? ? super : self
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,70 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "graphql/language/nodes/selection_ext"
|
3
|
-
require "graphql/client/query_result"
|
4
|
-
require "set"
|
5
|
-
|
6
|
-
module GraphQL
|
7
|
-
module Language
|
8
|
-
module Nodes
|
9
|
-
module Selections
|
10
|
-
# Public: Get GraphQL::QueryResult class for result of query.
|
11
|
-
#
|
12
|
-
# Returns subclass of QueryResult or nil.
|
13
|
-
def query_result_class(**kargs)
|
14
|
-
GraphQL::Client::QueryResult.define(fields: selections_query_result_classes(**kargs))
|
15
|
-
end
|
16
|
-
|
17
|
-
def selection_query_result_classes(**kargs)
|
18
|
-
selections_query_result_classes(**kargs)
|
19
|
-
end
|
20
|
-
|
21
|
-
# Internal: Gather QueryResult classes for each selection.
|
22
|
-
#
|
23
|
-
# Returns a Hash[String => (QueryResult|nil)].
|
24
|
-
def selections_query_result_classes(shadow: Set.new, **kargs)
|
25
|
-
self.selections.inject({}) do |h, selection|
|
26
|
-
case selection
|
27
|
-
when Selection
|
28
|
-
if !shadow.include?(selection)
|
29
|
-
selection.selection_query_result_classes(shadow: shadow, **kargs).each do |name, klass|
|
30
|
-
h[name] ? h[name] |= klass : h[name] = klass
|
31
|
-
end
|
32
|
-
end
|
33
|
-
h
|
34
|
-
else
|
35
|
-
raise TypeError, "expected selection to be of type Selection, but was #{selection.class}"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
class Field < AbstractNode
|
42
|
-
# Public: Get GraphQL::QueryResult class for result of query.
|
43
|
-
#
|
44
|
-
# Returns subclass of QueryResult or nil.
|
45
|
-
def query_result_class(**kargs)
|
46
|
-
if self.selections.any?
|
47
|
-
super
|
48
|
-
else
|
49
|
-
nil
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def selection_query_result_classes(**kargs)
|
54
|
-
name = self.alias || self.name
|
55
|
-
{ name => query_result_class(**kargs) }
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
class FragmentSpread < AbstractNode
|
60
|
-
def selection_query_result_classes(fragments: {}, shadow: Set.new, **kargs)
|
61
|
-
unless fragment = fragments[name.to_sym]
|
62
|
-
raise ArgumentError, "missing fragment '#{name}'"
|
63
|
-
end
|
64
|
-
return {} if shadow.include?(fragment)
|
65
|
-
fragment.selection_query_result_classes(fragments: fragments, shadow: shadow, **kargs)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "graphql/language/nodes/selection_ext"
|
3
|
-
|
4
|
-
module GraphQL
|
5
|
-
module Language
|
6
|
-
module Nodes
|
7
|
-
module Selections
|
8
|
-
def replace_fragment_spread(fragments)
|
9
|
-
other = self.dup
|
10
|
-
other.selections = self.selections.map do |selection|
|
11
|
-
case selection
|
12
|
-
when FragmentSpread
|
13
|
-
if fragment = fragments[selection.name.to_sym]
|
14
|
-
InlineFragment.new(type: fragment.type, directives: fragment.directives, selections: fragment.selections)
|
15
|
-
else
|
16
|
-
selection
|
17
|
-
end
|
18
|
-
when Selections
|
19
|
-
selection.replace_fragment_spread(fragments)
|
20
|
-
else
|
21
|
-
selection
|
22
|
-
end
|
23
|
-
end
|
24
|
-
other
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class Document < AbstractNode
|
29
|
-
def replace_fragment_spread(fragments)
|
30
|
-
other = self.dup
|
31
|
-
other.definitions = self.definitions.map do |definition|
|
32
|
-
case definition
|
33
|
-
when Selections
|
34
|
-
definition.replace_fragment_spread(fragments)
|
35
|
-
else
|
36
|
-
definition
|
37
|
-
end
|
38
|
-
end
|
39
|
-
other
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
module Language
|
5
|
-
module Nodes
|
6
|
-
# Public: Define shared trait for Nodes that have a "selections" collection.
|
7
|
-
module Selections
|
8
|
-
end
|
9
|
-
|
10
|
-
# Public: Define shared trait for Nodes that may be in a "selections" collection.
|
11
|
-
module Selection
|
12
|
-
end
|
13
|
-
|
14
|
-
class Field < AbstractNode
|
15
|
-
include Selection
|
16
|
-
include Selections
|
17
|
-
end
|
18
|
-
|
19
|
-
class FragmentDefinition < AbstractNode
|
20
|
-
include Selections
|
21
|
-
end
|
22
|
-
|
23
|
-
class FragmentSpread < AbstractNode
|
24
|
-
include Selection
|
25
|
-
end
|
26
|
-
|
27
|
-
class InlineFragment < AbstractNode
|
28
|
-
include Selection
|
29
|
-
include Selections
|
30
|
-
end
|
31
|
-
|
32
|
-
class OperationDefinition < AbstractNode
|
33
|
-
include Selections
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
class ValidationError < GraphQL::ExecutionError
|
5
|
-
end
|
6
|
-
|
7
|
-
module Language
|
8
|
-
module Nodes
|
9
|
-
class Document < AbstractNode
|
10
|
-
def validate!(schema:, rules: StaticValidation::ALL_RULES)
|
11
|
-
validator = StaticValidation::Validator.new(schema: schema, rules: rules)
|
12
|
-
query = Query.new(schema, document: self)
|
13
|
-
|
14
|
-
validator.validate(query).fetch(:errors).each do |error|
|
15
|
-
raise ValidationError, error["message"]
|
16
|
-
end
|
17
|
-
|
18
|
-
nil
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
class FragmentDefinition < AbstractNode
|
23
|
-
def validate!(schema:, **kargs)
|
24
|
-
document = Document.new(definitions: [self])
|
25
|
-
rules = StaticValidation::ALL_RULES - [StaticValidation::FragmentsAreUsed]
|
26
|
-
document.validate!(schema: schema, rules: rules, **kargs)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
class OperationDefinition < AbstractNode
|
31
|
-
def validate!(schema:, **kargs)
|
32
|
-
document = Document.new(definitions: [self])
|
33
|
-
document.validate!(schema: schema, **kargs)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
class InlineFragment < AbstractNode
|
38
|
-
def validate!(schema:, **kargs)
|
39
|
-
fragment = FragmentDefinition.new(name: "FooFragment", type: self.type, directives: self.directives, selections: self.selections)
|
40
|
-
fragment.validate!(schema: schema, **kargs)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "graphql/language/nodes/deep_freeze_ext"
|
3
|
-
|
4
|
-
module GraphQL
|
5
|
-
module Relay
|
6
|
-
NODE_QUERY = GraphQL.parse(<<-'GRAPHQL').definitions.first.deep_freeze
|
7
|
-
query($id: ID!) {
|
8
|
-
node(id: $id) {
|
9
|
-
...NodeFragment
|
10
|
-
}
|
11
|
-
}
|
12
|
-
GRAPHQL
|
13
|
-
|
14
|
-
def self.NodeQuery(fragment)
|
15
|
-
fragment = GraphQL::Language::Nodes::FragmentDefinition.new(name: "NodeFragment", type: fragment.type, selections: fragment.selections)
|
16
|
-
GraphQL::Language::Nodes::Document.new(definitions: [NODE_QUERY, fragment])
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,101 +0,0 @@
|
|
1
|
-
require "graphql"
|
2
|
-
require "json"
|
3
|
-
|
4
|
-
module GraphQL
|
5
|
-
class Schema
|
6
|
-
module JSONLoader
|
7
|
-
def self.define_schema(json)
|
8
|
-
schema = JSON.load(json).fetch("data").fetch("__schema")
|
9
|
-
types = Schema::JSONLoader.define_types(schema)
|
10
|
-
# TODO: handle schema["mutationType"]
|
11
|
-
# TODO: handle schema["subscriptionType"]
|
12
|
-
query = types.fetch(schema.fetch("queryType").fetch("name"))
|
13
|
-
Schema.new(query: query, types: types.values)
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.define_types(schema)
|
17
|
-
schema.fetch("types").inject({}) do |types, type|
|
18
|
-
type_kind, type_name = type.fetch("kind"), type.fetch("name")
|
19
|
-
|
20
|
-
if !type_name.start_with?("__")
|
21
|
-
case type_kind
|
22
|
-
when "INTERFACE"
|
23
|
-
types[type_name] = define_interface(types, type)
|
24
|
-
when "OBJECT"
|
25
|
-
types[type_name] = define_object(types, type)
|
26
|
-
when "SCALAR"
|
27
|
-
types[type_name] = define_scalar(types, type)
|
28
|
-
else
|
29
|
-
# TODO: handle other type kinds
|
30
|
-
fail NotImplementedError, type_kind + " not implemented"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
types
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.resolve_type(types, type)
|
39
|
-
case kind = type.fetch("kind")
|
40
|
-
when "INTERFACE"
|
41
|
-
types.fetch(type.fetch("name"))
|
42
|
-
when "LIST"
|
43
|
-
ListType.new(of_type: resolve_type(types, type.fetch("ofType")))
|
44
|
-
when "NON_NULL"
|
45
|
-
NonNullType.new(of_type: resolve_type(types, type.fetch("ofType")))
|
46
|
-
when "OBJECT"
|
47
|
-
types.fetch(type.fetch("name"))
|
48
|
-
when "SCALAR"
|
49
|
-
types.fetch(type.fetch("name"))
|
50
|
-
else
|
51
|
-
# TODO: handle other type kinds
|
52
|
-
fail NotImplementedError, kind + " not implemented"
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def self.define_interface(types, type)
|
57
|
-
InterfaceType.define do
|
58
|
-
name type.fetch("name")
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.define_object(types, type)
|
63
|
-
ObjectType.define do
|
64
|
-
name type.fetch("name")
|
65
|
-
description type["description"]
|
66
|
-
|
67
|
-
Array(type["fields"]).each do |field_data|
|
68
|
-
field field_data["name"] do
|
69
|
-
type JSONLoader.resolve_type(types, field_data["type"])
|
70
|
-
description field_data["description"]
|
71
|
-
field_data["args"].each do |arg|
|
72
|
-
argument arg["name"] do
|
73
|
-
type JSONLoader.resolve_type(types, arg["type"])
|
74
|
-
description arg["description"]
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def self.define_scalar(types, type)
|
83
|
-
case name = type.fetch("name")
|
84
|
-
when "Int"
|
85
|
-
INT_TYPE
|
86
|
-
when "String"
|
87
|
-
STRING_TYPE
|
88
|
-
when "Float"
|
89
|
-
FLOAT_TYPE
|
90
|
-
when "Boolean"
|
91
|
-
BOOLEAN_TYPE
|
92
|
-
when "ID"
|
93
|
-
ID_TYPE
|
94
|
-
else
|
95
|
-
# TODO: handle other scalar names
|
96
|
-
fail NotImplementedError, name + " scalar not implemented"
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|