graphql 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +78 -9
  3. data/lib/graphql/call.rb +7 -0
  4. data/lib/graphql/connection.rb +44 -0
  5. data/lib/graphql/field.rb +117 -0
  6. data/lib/graphql/field_definer.rb +29 -0
  7. data/lib/graphql/introspection/call_node.rb +13 -0
  8. data/lib/graphql/introspection/connection.rb +9 -0
  9. data/lib/graphql/introspection/field_node.rb +19 -0
  10. data/lib/graphql/introspection/root_call_argument_node.rb +5 -0
  11. data/lib/graphql/introspection/root_call_node.rb +16 -0
  12. data/lib/graphql/introspection/schema_call.rb +8 -0
  13. data/lib/graphql/introspection/schema_node.rb +17 -0
  14. data/lib/graphql/introspection/type_call.rb +8 -0
  15. data/lib/graphql/introspection/type_node.rb +16 -0
  16. data/lib/graphql/node.rb +141 -34
  17. data/lib/graphql/parser.rb +19 -8
  18. data/lib/graphql/query.rb +64 -21
  19. data/lib/graphql/root_call.rb +176 -0
  20. data/lib/graphql/root_call_argument.rb +8 -0
  21. data/lib/graphql/root_call_argument_definer.rb +20 -0
  22. data/lib/graphql/schema.rb +99 -0
  23. data/lib/graphql/syntax/call.rb +3 -12
  24. data/lib/graphql/syntax/field.rb +4 -2
  25. data/lib/graphql/syntax/node.rb +3 -10
  26. data/lib/graphql/syntax/query.rb +7 -0
  27. data/lib/graphql/syntax/variable.rb +7 -0
  28. data/lib/graphql/transform.rb +14 -5
  29. data/lib/graphql/types/boolean_field.rb +3 -0
  30. data/lib/graphql/types/connection_field.rb +30 -0
  31. data/lib/graphql/types/cursor_field.rb +9 -0
  32. data/lib/graphql/types/number_field.rb +3 -0
  33. data/lib/graphql/types/object_field.rb +8 -0
  34. data/lib/graphql/types/string_field.rb +3 -0
  35. data/lib/graphql/types/type_field.rb +6 -0
  36. data/lib/graphql/version.rb +3 -0
  37. data/readme.md +142 -10
  38. data/spec/graphql/field_spec.rb +66 -0
  39. data/spec/graphql/node_spec.rb +68 -0
  40. data/spec/graphql/parser_spec.rb +75 -25
  41. data/spec/graphql/query_spec.rb +185 -83
  42. data/spec/graphql/root_call_spec.rb +55 -0
  43. data/spec/graphql/schema_spec.rb +128 -0
  44. data/spec/graphql/transform_spec.rb +124 -39
  45. data/spec/spec_helper.rb +2 -1
  46. data/spec/support/dummy_app.rb +43 -23
  47. data/spec/support/nodes.rb +145 -32
  48. metadata +78 -16
  49. data/lib/graphql/collection_edge.rb +0 -62
  50. data/lib/graphql/syntax/edge.rb +0 -12
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe GraphQL::Field do
5
+ let(:owner) { OpenStruct.new(name: "TestOwner")}
6
+ let(:field) { GraphQL::Field.create_class(name: "high_fives", type: :number, owner_class: owner).new(query: {}) }
7
+
8
+ describe '#name' do
9
+ it 'is present' do
10
+ assert_equal field.name, "high_fives"
11
+ end
12
+ end
13
+
14
+ describe '#method' do
15
+ it 'defaults to name' do
16
+ assert_equal "high_fives", field.method
17
+ end
18
+ end
19
+
20
+ describe '.call' do
21
+ let(:content_field) { Nodes::PostNode.all_fields["content"] }
22
+ it 'doesnt register a call twice' do
23
+ assert_equal 3, content_field.calls.size
24
+ call = content_field.calls.first[1]
25
+ content_field.call(call.name, call.lambda)
26
+ content_field.call(call.name, call.lambda)
27
+ assert_equal 3, content_field.calls.size
28
+ end
29
+ end
30
+
31
+ describe '.to_s' do
32
+ it 'includes name' do
33
+ assert_match(/high_fives/, field.class.to_s)
34
+ end
35
+ it 'includes owner name' do
36
+ assert_match(/TestOwner/, field.class.to_s)
37
+ end
38
+ end
39
+
40
+ describe '__type__' do
41
+ let(:query_string) { "type(post) { fields { edges { node { name, type, calls { edges { node { name } }} } } } } "}
42
+ let(:query) { GraphQL::Query.new(query_string, context: {}) }
43
+ let(:result) { query.as_result }
44
+ let(:id_field) { result["post"]["fields"]["edges"][1]["node"] }
45
+ let(:title_field) { result["post"]["fields"]["edges"][2]["node"] }
46
+ let(:comments_field) { result["post"]["fields"]["edges"][5]["node"] }
47
+ let(:content_field) { result["post"]["fields"]["edges"][3]["node"] }
48
+
49
+ it 'has name' do
50
+ assert_equal "id", id_field["name"]
51
+ assert_equal "title", title_field["name"]
52
+ assert_equal "comments", comments_field["name"]
53
+ end
54
+
55
+ it 'has type' do
56
+ assert_equal "number", id_field["type"]
57
+ assert_equal "string", title_field["type"]
58
+ assert_equal "connection", comments_field["type"]
59
+ end
60
+
61
+ it 'has calls' do
62
+ assert_equal 3, content_field["calls"]["edges"].length
63
+ assert_equal ["from", "for", "select"], content_field["calls"]["edges"].map {|c| c["node"]["name"] }
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe GraphQL::Node do
4
+ let(:query_string) { "type(post) { name, description, fields { count, edges { node { name, description }}} }"}
5
+ let(:result) { GraphQL::Query.new(query_string).as_result}
6
+
7
+ describe '__type__' do
8
+ let(:title_field) { result["post"]["fields"]["edges"].find {|e| e["node"]["name"] == "title"}["node"] }
9
+ it 'has name' do
10
+ assert_equal "post", result["post"]["name"]
11
+ end
12
+
13
+ it 'has description' do
14
+ assert_equal "A blog post entry", result["post"]["description"]
15
+ end
16
+
17
+ it 'has fields' do
18
+ assert_equal 8, result["post"]["fields"]["count"]
19
+ assert_equal({ "name" => "title", "description" => nil}, title_field)
20
+ end
21
+
22
+ describe 'getting the __type__ field' do
23
+ before do
24
+ @post = Post.create(id: 155, content: "Hello world")
25
+ end
26
+
27
+ after do
28
+ @post.destroy
29
+ end
30
+
31
+ let(:query_string) { "post(155) { __type__ { name, fields { count } } }"}
32
+
33
+ it 'exposes the type' do
34
+ assert_equal "post", result["155"]["__type__"]["name"]
35
+ assert_equal 8, result["155"]["__type__"]["fields"]["count"]
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '.node_name' do
41
+ let(:query_string) { "type(upvote) { name }"}
42
+
43
+ it 'overrides __type__.name' do
44
+ assert_equal "upvote", result["upvote"]["name"]
45
+ end
46
+ end
47
+
48
+ describe '.field' do
49
+ it 'doesnt add the field twice if you call it twice' do
50
+ assert_equal 5, Nodes::CommentNode.all_fields.size
51
+ Nodes::CommentNode.field.number(:id)
52
+ Nodes::CommentNode.field.number(:id)
53
+ assert_equal 5, Nodes::CommentNode.all_fields.size
54
+ Nodes::CommentNode.remove_field(:id)
55
+ end
56
+
57
+ describe 'type:' do
58
+ it 'uses symbols to find built-ins' do
59
+ id_field = Nodes::CommentNode.all_fields["id"]
60
+ assert id_field.superclass == GraphQL::Types::NumberField
61
+ end
62
+ it 'uses the provided class as a superclass' do
63
+ letters_field = Nodes::CommentNode.all_fields["letters"]
64
+ assert letters_field.superclass == Nodes::LetterSelectionField
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,33 +1,59 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe GraphQL::Parser do
4
- let(:node_name) { "" }
5
- let(:fields) { "id, name"}
6
- let(:query) { "#{node_name} { #{fields} }"}
7
4
  let(:parser) { GraphQL::PARSER }
8
5
 
6
+ describe 'query' do
7
+ let(:query) { parser.query }
8
+ it 'parses node-only' do
9
+ assert query.parse_with_debug("node(4) { id, name } ")
10
+ end
11
+ it 'parses node and variables' do
12
+ assert query.parse_with_debug(%{
13
+ like_page(<page>) {
14
+ page { id }
15
+ }
16
+ <page>: {
17
+ "page": {"id": 1},
18
+ "person" : { "id", 4}
19
+ }
20
+ <other>: {
21
+ "page": {"id": 1},
22
+ "person" : { "id", 4}
23
+ }
24
+ })
25
+ end
26
+ end
9
27
  describe 'field' do
10
28
  let(:field) { parser.field }
11
29
  it 'finds words' do
12
- assert field.parse_with_debug("name")
13
30
  assert field.parse_with_debug("date_of_birth")
14
31
  end
15
- end
16
32
 
17
- describe 'edge' do
18
- let(:edge) { parser.edge }
33
+ it 'finds aliases' do
34
+ assert field.parse_with_debug("name as moniker")
35
+ end
19
36
 
20
37
  it 'finds calls on fields' do
21
- assert edge.parse_with_debug("friends.first(1) {
22
- count,
23
- edges {
24
- cursor,
25
- node {
26
- name
27
- }
28
- }
29
- }
30
- ")
38
+ assert field.parse_with_debug("url.site(www).upcase()")
39
+ end
40
+
41
+ describe 'fields that return objects' do
42
+ it 'finds them' do
43
+ assert field.parse_with_debug("birthdate { month, year }")
44
+ end
45
+
46
+ it 'finds them with aliases' do
47
+ assert field.parse_with_debug("birthdate as d_o_b { month, year }")
48
+ end
49
+
50
+ it 'finds them with calls' do
51
+ assert field.parse_with_debug("friends.after(123) { count { edges { node { id } } } }")
52
+ end
53
+
54
+ it 'finds them with calls and aliases' do
55
+ assert field.parse_with_debug("friends.after(123) as pals { count { edges { node { id } } } }")
56
+ end
31
57
  end
32
58
  end
33
59
 
@@ -37,15 +63,13 @@ describe GraphQL::Parser do
37
63
  assert call.parse_with_debug("node(123)")
38
64
  assert call.parse_with_debug("viewer()")
39
65
  end
40
- end
41
66
 
42
- describe 'call_chain' do
43
- let(:call_chain) { parser.call_chain }
44
- it 'finds deep calls' do
45
- assert call_chain.parse_with_debug("friends.after(123).first(2)")
67
+ it 'finds calls with multiple arguments' do
68
+ assert call.parse_with_debug("node(4, 6)")
46
69
  end
47
- it 'finds chain with no calls' do
48
- assert call_chain.parse_with_debug("friends")
70
+
71
+ it 'finds calls with variables' do
72
+ assert call.parse_with_debug("like_page(<page>)")
49
73
  end
50
74
  end
51
75
 
@@ -71,7 +95,8 @@ describe GraphQL::Parser do
71
95
  end
72
96
 
73
97
  it 'parses nested nodes' do
74
- assert node.parse_with_debug("node(someone)
98
+ assert node.parse_with_debug("
99
+ node(someone)
75
100
  {
76
101
  id,
77
102
  name,
@@ -86,4 +111,29 @@ describe GraphQL::Parser do
86
111
  ")
87
112
  end
88
113
  end
114
+
115
+ describe 'variable' do
116
+ let(:variable) { parser.variable }
117
+
118
+ it 'gets scalar variables' do
119
+ assert variable.parse_with_debug(%{<some_number>: 888})
120
+ assert variable.parse_with_debug(%{<some_string>: my_string})
121
+ end
122
+ it 'gets json variables' do
123
+ assert variable.parse_with_debug(%{<my_input>: {"key": "value"}})
124
+ end
125
+
126
+ it 'gets variables with nesting' do
127
+ assert variable.parse_with_debug(%{
128
+ <my_input>: {
129
+ "key": "value",
130
+ "1": 2,
131
+ "true": false,
132
+ "nested": {
133
+ "key" : "value"
134
+ }
135
+ }
136
+ })
137
+ end
138
+ end
89
139
  end
@@ -2,153 +2,255 @@ require 'spec_helper'
2
2
 
3
3
  describe GraphQL::Query do
4
4
  let(:query_string) { "post(123) { title, content } "}
5
- let(:namespace) { Nodes }
6
- let(:query) { GraphQL::Query.new(query_string, namespace: namespace) }
5
+ let(:context) { Context.new(person_name: "Han Solo") }
6
+ let(:query) { GraphQL::Query.new(query_string, context: context) }
7
+ let(:result) { query.as_result }
7
8
 
8
- describe '#root' do
9
- it 'contains the first node of the graph' do
10
- assert query.root.is_a?(GraphQL::Syntax::Node)
11
- end
9
+ before do
10
+ @post = Post.create(id: 123, content: "So many great things", title: "My great post", published_at: Date.new(2010,1,4))
11
+ @comment1 = Comment.create(id: 444, post_id: 123, content: "I agree", rating: 5)
12
+ @comment2 = Comment.create(id: 445, post_id: 123, content: "I disagree", rating: 1)
13
+ @like1 = Like.create(id: 991, post_id: 123)
14
+ @like2 = Like.create(id: 992, post_id: 123)
15
+ end
16
+
17
+ after do
18
+ @post.destroy
19
+ @comment1.destroy
20
+ @comment2.destroy
21
+ @like1.destroy
22
+ @like2.destroy
12
23
  end
13
24
 
14
- describe '#to_json' do
15
- before do
16
- @post = Post.create(id: 123, content: "So many great things", title: "My great post")
17
- @comment1 = Comment.create(id: 444, post_id: 123, content: "I agree")
18
- @comment2 = Comment.create(id: 445, post_id: 123, content: "I disagree")
25
+ describe '#as_result' do
26
+ it 'finds fields that delegate to a target' do
27
+ assert_equal result, {"123" => {"title" => "My great post", "content" => "So many great things"}}
19
28
  end
20
29
 
21
- after do
22
- @post.destroy
23
- @comment1.destroy
24
- @comment2.destroy
30
+ describe 'with multiple roots' do
31
+ let(:query_string) { "comment(444, 445) { content } "}
32
+ it 'adds each as a key-value of the response' do
33
+ assert_equal ["444", "445"], result.keys
34
+ end
25
35
  end
26
36
 
27
- it 'performs the root node call' do
28
- assert_send([Nodes::PostNode, :call, "123"])
29
- query.to_json
37
+ describe 'when accessing fields that return objects' do
38
+ describe 'when making calls on the field' do
39
+ let(:query_string) { "post(123) { published_at.minus_days(200) { year } }"}
40
+ it 'returns the modified value' do
41
+ assert_equal 2009, result["123"]["published_at"]["year"]
42
+ end
43
+ end
44
+ describe 'when requesting more fields' do
45
+ let(:query_string) { "post(123) { published_at { month, year } }"}
46
+ it 'returns those fields' do
47
+ assert_equal({"month" => 1, "year" => 2010}, result["123"]["published_at"])
48
+ end
49
+ end
30
50
  end
51
+ describe 'when aliasing things' do
52
+ let(:query_string) { "post(123) { title as headline, content as what_it_says }"}
31
53
 
32
- it 'finds fields that delegate to a target' do
33
- assert_equal query.to_json, {
34
- "123" => {
35
- "title" => "My great post",
36
- "content" => "So many great things"
37
- }
38
- }
54
+ it 'applies aliases to fields' do
55
+ assert_equal @post.title, result["123"]["headline"]
56
+ assert_equal @post.content, result["123"]["what_it_says"]
57
+ end
58
+
59
+ it 'applies aliases to edges' # dunno the syntax yet
39
60
  end
40
61
 
41
62
  describe 'when requesting fields defined on the node' do
42
- let(:query_string) { "post(123) { teaser } "}
63
+ let(:query_string) { "post(123) { length } "}
43
64
  it 'finds fields defined on the node' do
44
- assert_equal query.to_json, { "123" => { "teaser" => @post.content[0,10] + "..."}}
65
+ assert_equal 20, result["123"]["length"]
45
66
  end
46
67
  end
47
68
 
69
+ describe 'when accessing custom fields' do
70
+ let(:query_string) { "comment(444) { letters }"}
71
+ it 'uses the custom field' do
72
+ assert_equal "I agree", result["444"]["letters"]
73
+ end
74
+
75
+ describe 'when making calls on fields' do
76
+ let(:query_string) { "comment(444) {
77
+ letters.select(4, 3),
78
+ letters.from(3).for(2) as snippet
79
+ }"}
80
+
81
+ it 'works with aliases' do
82
+ assert result["444"]["snippet"].present?
83
+ end
84
+
85
+ it 'applies calls' do
86
+ assert_equal "gr", result["444"]["snippet"]
87
+ end
88
+
89
+ it 'applies calls with multiple arguments' do
90
+ assert_equal "ree", result["444"]["letters"]
91
+ end
92
+ end
93
+
94
+ describe 'when requesting fields overriden on a child class' do
95
+ let(:query_string) { 'thumb_up(991) { id }'}
96
+ it 'uses the child implementation' do
97
+ assert_equal '991991', result["991991"]["id"]
98
+ end
99
+ end
100
+ end
48
101
 
49
102
  describe 'when requesting an undefined field' do
50
103
  let(:query_string) { "post(123) { destroy } "}
51
104
  it 'raises a FieldNotDefined error' do
52
- assert_raises(GraphQL::FieldNotDefinedError) { query.to_json }
105
+ assert_raises(GraphQL::FieldNotDefinedError) { query.as_result }
53
106
  assert(Post.find(123).present?)
54
107
  end
55
108
  end
56
109
 
57
110
  describe 'when the root call doesnt have an argument' do
58
- let(:query_string) { "viewer() { name }"}
59
- it 'calls the node with nil' do
60
- assert_send([Nodes::ViewerNode, :call, nil])
61
- query.to_json
111
+ let(:query_string) { "context() { person_name }"}
112
+ it 'calls the node with no arguments' do
113
+ assert_equal "Han Solo", result["context"]["person_name"]
62
114
  end
63
115
  end
64
116
 
65
117
  describe 'when requesting a collection' do
66
118
  let(:query_string) { "post(123) {
67
119
  title,
68
- comments {
69
- count,
70
- edges {
71
- cursor,
72
- node {
73
- content
74
- }
75
- }
76
- }
120
+ comments { count, edges { cursor, node { content } } }
77
121
  }"}
122
+
78
123
  it 'returns collection data' do
79
- assert_equal query.to_json, {
124
+ assert_equal result, {
80
125
  "123" => {
81
126
  "title" => "My great post",
82
127
  "comments" => {
83
128
  "count" => 2,
84
129
  "edges" => [
85
- {
86
- "cursor" => "444",
87
- "node" => {
88
- "content" => "I agree"
89
- }
90
- },
91
- {
92
- "cursor" => "445",
93
- "node" => {
94
- "content" => "I disagree"
95
- }
96
- }
130
+ { "cursor" => "444", "node" => {"content" => "I agree"} },
131
+ { "cursor" => "445", "node" => {"content" => "I disagree"}}
97
132
  ]
98
- }
99
- }
100
- }
133
+ }}}
101
134
  end
102
135
  end
103
136
 
104
137
  describe 'when making calls on a collection' do
105
- let(:query_string) { "post(123) {
106
- comments.first(1) {
107
- edges { cursor, node { content } }
108
- }
109
- }"}
138
+ let(:query_string) { "post(123) { comments.first(1) { edges { cursor, node { content } } } }"}
110
139
 
111
140
  it 'executes those calls' do
112
- assert_equal query.to_json, {
141
+ assert_equal result, {
113
142
  "123" => {
114
143
  "comments" => {
115
144
  "edges" => [
116
- {
117
- "cursor" => "444",
118
- "node" => {
119
- "content" => "I agree"
120
- }
121
- }
145
+ { "cursor" => "444", "node" => { "content" => "I agree"} }
122
146
  ]
123
- }
124
- }
125
- }
147
+ }}}
126
148
  end
127
149
  end
128
150
 
129
151
  describe 'when making DEEP calls on a collection' do
130
- let(:query_string) { "post(123) {
131
- comments.after(444).first(1) {
152
+ let(:query_string) { "post(123) { comments.after(444).first(1) {
132
153
  edges { cursor, node { content } }
133
- }
134
- }"}
154
+ }}"}
135
155
 
136
156
  it 'executes those calls' do
137
- assert_equal query.to_json, {
157
+ assert_equal result, {
138
158
  "123" => {
139
159
  "comments" => {
140
160
  "edges" => [
141
161
  {
142
162
  "cursor" => "445",
143
- "node" => {
144
- "content" => "I disagree"
145
- }
163
+ "node" => { "content" => "I disagree"}
146
164
  }
147
165
  ]
148
- }
149
- }
150
- }
166
+ }}}
151
167
  end
152
168
  end
169
+
170
+ describe 'when requesting fields at collection-level' do
171
+ let(:query_string) { "post(123) { comments { average_rating } }"}
172
+
173
+ it 'executes those calls' do
174
+ assert_equal result, { "123" => { "comments" => { "average_rating" => 3 } } }
175
+ end
176
+ end
177
+
178
+ describe 'when making calls on node fields' do
179
+ let(:query_string) { "post(123) { comments { edges { node { letters.from(3).for(3) }} } }"}
180
+
181
+ it 'makes calls on the fields' do
182
+ assert_equal ["gre", "isa"], result["123"]["comments"]["edges"].map {|e| e["node"]["letters"] }
183
+ end
184
+ end
185
+
186
+ describe 'when requesting collection-level fields that dont exist' do
187
+ let(:query_string) { "post(123) { comments { bogus_field } }"}
188
+
189
+ it 'raises FieldNotDefined' do
190
+ assert_raises(GraphQL::FieldNotDefinedError) { query.as_result }
191
+ end
192
+ end
193
+ end
194
+
195
+ describe 'when requesting fields on a related object' do
196
+ let(:query_string) { "comment(444) { post { title } }"}
197
+ it 'finds fields on that object' do
198
+ assert_equal "My great post", result["444"]["post"]["title"]
199
+ end
200
+
201
+ describe 'when the object doesnt exist' do
202
+ before do
203
+ Post.all.map(&:destroy)
204
+ end
205
+
206
+ it 'blows_up' do # what _should_ this do?
207
+ assert_raises(RuntimeError) { result }
208
+ end
209
+ end
210
+ end
211
+
212
+ describe 'when edge classes were named explicitly' do
213
+ let(:query_string) { "post(123) { likes { any, edges { node { id } } } }"}
214
+
215
+ it 'gets node values' do
216
+ assert_equal ["991991","992992"], result["123"]["likes"]["edges"].map {|e| e["node"]["id"] }
217
+ end
218
+
219
+ it 'gets edge values' do
220
+ assert_equal true, result["123"]["likes"]["any"]
221
+ end
222
+ end
223
+
224
+ describe '#context' do
225
+ let(:query_string) { "context() { person_name }"}
226
+
227
+ it 'is accessible inside nodes' do
228
+ assert_equal({"context" => {"person_name" => "Han Solo"}}, result)
229
+ end
230
+
231
+ describe 'inside edges' do
232
+ let(:query_string) { "post(123) { comments { viewer_name_length } }"}
233
+ it 'is accessible' do
234
+ assert_equal 8, result["123"]["comments"]["viewer_name_length"]
235
+ end
236
+ end
237
+ end
238
+
239
+ describe 'parsing error' do
240
+ let(:query_string) { "\n\n<< bogus >>"}
241
+
242
+ it 'raises SyntaxError' do
243
+ assert_raises(GraphQL::SyntaxError) { result }
244
+ end
245
+
246
+ it 'contains line an character number' do
247
+ err = assert_raises(GraphQL::SyntaxError) { result }
248
+ assert_match(/1, 1/, err.to_s)
249
+ end
250
+
251
+ it 'contains sample of text' do
252
+ err = assert_raises(GraphQL::SyntaxError) { result }
253
+ assert_includes(err.to_s, "<< bogus >>")
254
+ end
153
255
  end
154
256
  end