graphql 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/lib/graphql.rb +26 -0
- data/lib/graphql/collection_edge.rb +62 -0
- data/lib/graphql/node.rb +61 -0
- data/lib/graphql/parser.rb +20 -0
- data/lib/graphql/query.rb +41 -0
- data/lib/graphql/syntax/call.rb +17 -0
- data/lib/graphql/syntax/edge.rb +12 -0
- data/lib/graphql/syntax/field.rb +7 -0
- data/lib/graphql/syntax/node.rb +15 -0
- data/lib/graphql/transform.rb +10 -0
- data/readme.md +18 -0
- data/spec/graphql/parser_spec.rb +89 -0
- data/spec/graphql/query_spec.rb +154 -0
- data/spec/graphql/transform_spec.rb +58 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/dummy_app.rb +61 -0
- data/spec/support/nodes.rb +59 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c86e3ad590a80d6430b2fd781fb097e433d5e5c4
|
4
|
+
data.tar.gz: 60d41b9666ffaa6d63a73238fd572fdbbba38218
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 27b1a4571a57b15e35dff3dcab058d8f8b9926c374361a6df52930296a1be705a49a5b345e23ad128d73eb81675a0b798e0b225c0c5fc92904cb76ebf3aaccfe
|
7
|
+
data.tar.gz: 5594ef107005a1d7abca4e9f6099a6c4f515176679d4421126e7a332a433271f367d447a981263a89accd573b8060ec22b1ee7f71809cb5a8c23e19d534eccee
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Robert Mosolgo
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/graphql.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require "parslet"
|
2
|
+
require "active_support/core_ext/string/inflections"
|
3
|
+
require "active_support/core_ext/object/blank"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
VERSION = "0.0.1"
|
7
|
+
|
8
|
+
autoload(:CollectionEdge, "graphql/collection_edge")
|
9
|
+
autoload(:Parser, "graphql/parser")
|
10
|
+
autoload(:Query, "graphql/query")
|
11
|
+
autoload(:Node, "graphql/node")
|
12
|
+
autoload(:Transform, "graphql/transform")
|
13
|
+
|
14
|
+
module Syntax
|
15
|
+
autoload(:Call, "graphql/syntax/call")
|
16
|
+
autoload(:Edge, "graphql/syntax/edge")
|
17
|
+
autoload(:Field, "graphql/syntax/field")
|
18
|
+
autoload(:Node, "graphql/syntax/node")
|
19
|
+
end
|
20
|
+
|
21
|
+
PARSER = Parser.new
|
22
|
+
TRANSFORM = Transform.new
|
23
|
+
|
24
|
+
class FieldNotDefinedError < RuntimeError
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class GraphQL::CollectionEdge
|
2
|
+
attr_accessor :fields, :edge_class, :calls, :fields
|
3
|
+
|
4
|
+
def initialize(items:, edge_class:)
|
5
|
+
@items = items
|
6
|
+
@edge_class = edge_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_json
|
10
|
+
json = {}
|
11
|
+
fields.each do |field|
|
12
|
+
name = field.identifier
|
13
|
+
if name == "edges"
|
14
|
+
json["edges"] = edges(fields: field.fields)
|
15
|
+
else
|
16
|
+
json[name] = safe_send(name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
json
|
20
|
+
end
|
21
|
+
|
22
|
+
def count
|
23
|
+
@items.count
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply_calls(unfiltered_items, call_hash)
|
27
|
+
# override this to apply calls to your items
|
28
|
+
unfiltered_items
|
29
|
+
end
|
30
|
+
|
31
|
+
def edges(fields:)
|
32
|
+
filtered_items = apply_calls(items, calls)
|
33
|
+
filtered_items.map do |item|
|
34
|
+
node = edge_class.new(item)
|
35
|
+
json = {}
|
36
|
+
fields.each do |field|
|
37
|
+
name = field.identifier
|
38
|
+
if name == "node" # it's magic
|
39
|
+
node.fields = field.fields
|
40
|
+
json[name] = node.to_json
|
41
|
+
else
|
42
|
+
json[name] = node.safe_send(name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
json
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def safe_send(identifier)
|
50
|
+
if respond_to?(identifier)
|
51
|
+
public_send(identifier)
|
52
|
+
else
|
53
|
+
raise GraphQL::FieldNotDefinedError, "#{self.class.name}##{identifier} was requested, but it isn't defined."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def items
|
60
|
+
@items
|
61
|
+
end
|
62
|
+
end
|
data/lib/graphql/node.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
class GraphQL::Node
|
2
|
+
attr_accessor :fields
|
3
|
+
|
4
|
+
def initialize(target=nil)
|
5
|
+
# DONT EXPOSE Node#target! otherwise you might be able to access it
|
6
|
+
@target = target
|
7
|
+
end
|
8
|
+
|
9
|
+
def safe_send(identifier)
|
10
|
+
if respond_to?(identifier)
|
11
|
+
public_send(identifier)
|
12
|
+
else
|
13
|
+
raise GraphQL::FieldNotDefinedError, "#{self.class.name}##{identifier} was requested, but it isn't defined."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_json
|
18
|
+
json = {}
|
19
|
+
fields.each do |field|
|
20
|
+
name = field.identifier
|
21
|
+
if field.is_a?(GraphQL::Syntax::Field)
|
22
|
+
json[name] = safe_send(name)
|
23
|
+
elsif field.is_a?(GraphQL::Syntax::Edge)
|
24
|
+
edge = safe_send(field.identifier)
|
25
|
+
edge.calls = field.call_hash
|
26
|
+
edge.fields = field.fields
|
27
|
+
json[name] = edge.to_json
|
28
|
+
end
|
29
|
+
end
|
30
|
+
json
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def self.call(argument)
|
35
|
+
raise NotImplementedError, "Implement #{name}#call(argument) to use this node as a call"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.field_reader(*field_names)
|
39
|
+
field_names.each do |field_name|
|
40
|
+
define_method(field_name) do
|
41
|
+
@target.public_send(field_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.edges(field_name, collection_class_name:, edge_class_name:)
|
47
|
+
define_method(field_name) do
|
48
|
+
collection_items = @target.send(field_name)
|
49
|
+
collection_class = Object.const_get(collection_class_name)
|
50
|
+
edge_class = Object.const_get(edge_class_name)
|
51
|
+
collection = collection_class.new(items: collection_items, edge_class: edge_class)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def self.cursor(field_name)
|
57
|
+
define_method "cursor" do
|
58
|
+
safe_send(field_name).to_s
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class GraphQL::Parser < Parslet::Parser
|
2
|
+
root(:node)
|
3
|
+
|
4
|
+
rule(:node) { space? >> call >> space? >> fields.as(:fields) }
|
5
|
+
|
6
|
+
rule(:fields) { str("{") >> space? >> ((edge | field) >> str(",").maybe >> space?).repeat(1) >> space? >> str("}") >> space?}
|
7
|
+
|
8
|
+
rule(:edge) { call_chain >> space? >> fields.as(:fields) }
|
9
|
+
rule(:call_chain) { identifier >> (dot >> call).repeat(0).as(:calls) }
|
10
|
+
|
11
|
+
rule(:call) { identifier >> str("(") >> name.maybe.as(:argument) >> str(")") }
|
12
|
+
rule(:dot) { str(".") }
|
13
|
+
|
14
|
+
rule(:field) { identifier }
|
15
|
+
|
16
|
+
rule(:identifier) { name.as(:identifier) }
|
17
|
+
rule(:name) { match('\w').repeat(1) }
|
18
|
+
rule(:space) { match('[\s\n]+').repeat(1) }
|
19
|
+
rule(:space?) { space.maybe }
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class GraphQL::Query
|
2
|
+
attr_reader :query_string, :root, :namespace
|
3
|
+
def initialize(query_string, namespace: Object)
|
4
|
+
if !query_string.is_a?(String) || query_string.length == 0
|
5
|
+
raise "You must send a query string, not a #{query_string.class.name}"
|
6
|
+
end
|
7
|
+
@query_string = query_string
|
8
|
+
@root = parse(query_string)
|
9
|
+
@namespace = namespace
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_json
|
13
|
+
root_node = make_call(nil, root.identifier, root.argument)
|
14
|
+
raise "Couldn't find root for #{root.identifier}(#{root.argument})" if root.nil?
|
15
|
+
|
16
|
+
root_node.fields = root.fields
|
17
|
+
{
|
18
|
+
root_node.cursor => root_node.to_json
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_node(identifier)
|
23
|
+
name = "#{identifier}_node"
|
24
|
+
namespace.const_get(name.camelize)
|
25
|
+
end
|
26
|
+
|
27
|
+
def make_call(context, name, *arguments)
|
28
|
+
if context.nil?
|
29
|
+
context = get_node(name)
|
30
|
+
name = "call"
|
31
|
+
end
|
32
|
+
context.send(name, *arguments)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def parse(query_string)
|
38
|
+
parsed_hash = GraphQL::PARSER.parse(query_string)
|
39
|
+
root_node = GraphQL::TRANSFORM.apply(parsed_hash)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class GraphQL::Syntax::Call
|
2
|
+
attr_reader :identifier, :argument, :calls
|
3
|
+
def initialize(identifier:, argument: nil, calls: [])
|
4
|
+
@identifier = identifier
|
5
|
+
@argument = argument
|
6
|
+
@calls = calls
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute!(query)
|
10
|
+
node_class = query.get_node(identifier)
|
11
|
+
node_class.call(argument)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_query
|
15
|
+
(["#{identifier}(#{argument})"] + calls.map(&:to_query)).join(".")
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class GraphQL::Syntax::Edge
|
2
|
+
attr_reader :identifier, :fields, :calls
|
3
|
+
def initialize(identifier:, fields:, calls:)
|
4
|
+
@identifier = identifier
|
5
|
+
@calls = calls
|
6
|
+
@fields = fields
|
7
|
+
end
|
8
|
+
|
9
|
+
def call_hash
|
10
|
+
calls.inject({}) { |memo, call| memo[call.identifier] = call.argument; memo }
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class GraphQL::Syntax::Node
|
2
|
+
attr_reader :identifier, :argument, :fields
|
3
|
+
def initialize(identifier:, argument:, fields: [])
|
4
|
+
@identifier = identifier
|
5
|
+
@argument = argument
|
6
|
+
@fields = fields
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute!(query)
|
10
|
+
obj = identifier.execute!(query)
|
11
|
+
fields.each do |field|
|
12
|
+
obj.apply_field(field)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class GraphQL::Transform < Parslet::Transform
|
2
|
+
# node
|
3
|
+
rule(identifier: simple(:i), argument: simple(:a), fields: sequence(:f)) {GraphQL::Syntax::Node.new(identifier: i.to_s, argument: a, fields: f)}
|
4
|
+
# edge
|
5
|
+
rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f)) { GraphQL::Syntax::Edge.new(identifier: i.to_s, fields: f, calls: c)}
|
6
|
+
# field
|
7
|
+
rule(identifier: simple(:i)) { GraphQL::Syntax::Field.new(identifier: i.to_s)}
|
8
|
+
# call
|
9
|
+
rule(identifier: simple(:i), argument: simple(:a)) { GraphQL::Syntax::Call.new(identifier: i.to_s, argument: a) }
|
10
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# graphql
|
2
|
+
|
3
|
+
- Parser & tranform from [parslet](http://kschiess.github.io/parslet/)
|
4
|
+
- Your app can implement nodes
|
5
|
+
- You can pass strings to `GraphQL::Query` and execute them with your nodes
|
6
|
+
|
7
|
+
See `/spec/support/dummy_app/nodes.rb` for node examples
|
8
|
+
|
9
|
+
__Nodes__ provide information to queries by mapping to application objects (via `.call` and `field_reader`) or implementing fields themselves (eg `Nodes::PostNode#teaser`).
|
10
|
+
|
11
|
+
__Edges__ handle node-to-node relationships.
|
12
|
+
|
13
|
+
|
14
|
+
## To do:
|
15
|
+
|
16
|
+
- Better class inference. Declaring edge classes is stupid.
|
17
|
+
- How to authenticate?
|
18
|
+
- What do graphql mutation queries even look like?
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GraphQL::Parser do
|
4
|
+
let(:node_name) { "" }
|
5
|
+
let(:fields) { "id, name"}
|
6
|
+
let(:query) { "#{node_name} { #{fields} }"}
|
7
|
+
let(:parser) { GraphQL::PARSER }
|
8
|
+
|
9
|
+
describe 'field' do
|
10
|
+
let(:field) { parser.field }
|
11
|
+
it 'finds words' do
|
12
|
+
assert field.parse_with_debug("name")
|
13
|
+
assert field.parse_with_debug("date_of_birth")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'edge' do
|
18
|
+
let(:edge) { parser.edge }
|
19
|
+
|
20
|
+
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
|
+
")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'call' do
|
35
|
+
let(:call) { parser.call }
|
36
|
+
it 'finds bare calls' do
|
37
|
+
assert call.parse_with_debug("node(123)")
|
38
|
+
assert call.parse_with_debug("viewer()")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
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)")
|
46
|
+
end
|
47
|
+
it 'finds chain with no calls' do
|
48
|
+
assert call_chain.parse_with_debug("friends")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'fields' do
|
53
|
+
let(:fields) { parser.fields }
|
54
|
+
|
55
|
+
it 'finds fields' do
|
56
|
+
assert fields.parse_with_debug("{id,name}")
|
57
|
+
assert fields.parse_with_debug("{ id, name, favorite_food }")
|
58
|
+
assert fields.parse_with_debug("{\n id,\n name,\n favorite_food\n}")
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'finds nested field list' do
|
62
|
+
assert fields.parse_with_debug("{id,date_of_birth{month, year}}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe 'node' do
|
67
|
+
let(:node) { parser.node }
|
68
|
+
|
69
|
+
it 'parses root calls' do
|
70
|
+
assert node.parse_with_debug("viewer() {id}")
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'parses nested nodes' do
|
74
|
+
assert node.parse_with_debug("node(someone)
|
75
|
+
{
|
76
|
+
id,
|
77
|
+
name,
|
78
|
+
friends.after(12345).first(3) {
|
79
|
+
cursor,
|
80
|
+
node {
|
81
|
+
id,
|
82
|
+
name
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GraphQL::Query do
|
4
|
+
let(:query_string) { "post(123) { title, content } "}
|
5
|
+
let(:namespace) { Nodes }
|
6
|
+
let(:query) { GraphQL::Query.new(query_string, namespace: namespace) }
|
7
|
+
|
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
|
12
|
+
end
|
13
|
+
|
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")
|
19
|
+
end
|
20
|
+
|
21
|
+
after do
|
22
|
+
@post.destroy
|
23
|
+
@comment1.destroy
|
24
|
+
@comment2.destroy
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'performs the root node call' do
|
28
|
+
assert_send([Nodes::PostNode, :call, "123"])
|
29
|
+
query.to_json
|
30
|
+
end
|
31
|
+
|
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
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'when requesting fields defined on the node' do
|
42
|
+
let(:query_string) { "post(123) { teaser } "}
|
43
|
+
it 'finds fields defined on the node' do
|
44
|
+
assert_equal query.to_json, { "123" => { "teaser" => @post.content[0,10] + "..."}}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
describe 'when requesting an undefined field' do
|
50
|
+
let(:query_string) { "post(123) { destroy } "}
|
51
|
+
it 'raises a FieldNotDefined error' do
|
52
|
+
assert_raises(GraphQL::FieldNotDefinedError) { query.to_json }
|
53
|
+
assert(Post.find(123).present?)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
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
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'when requesting a collection' do
|
66
|
+
let(:query_string) { "post(123) {
|
67
|
+
title,
|
68
|
+
comments {
|
69
|
+
count,
|
70
|
+
edges {
|
71
|
+
cursor,
|
72
|
+
node {
|
73
|
+
content
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}"}
|
78
|
+
it 'returns collection data' do
|
79
|
+
assert_equal query.to_json, {
|
80
|
+
"123" => {
|
81
|
+
"title" => "My great post",
|
82
|
+
"comments" => {
|
83
|
+
"count" => 2,
|
84
|
+
"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
|
+
}
|
97
|
+
]
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
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
|
+
}"}
|
110
|
+
|
111
|
+
it 'executes those calls' do
|
112
|
+
assert_equal query.to_json, {
|
113
|
+
"123" => {
|
114
|
+
"comments" => {
|
115
|
+
"edges" => [
|
116
|
+
{
|
117
|
+
"cursor" => "444",
|
118
|
+
"node" => {
|
119
|
+
"content" => "I agree"
|
120
|
+
}
|
121
|
+
}
|
122
|
+
]
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe 'when making DEEP calls on a collection' do
|
130
|
+
let(:query_string) { "post(123) {
|
131
|
+
comments.after(444).first(1) {
|
132
|
+
edges { cursor, node { content } }
|
133
|
+
}
|
134
|
+
}"}
|
135
|
+
|
136
|
+
it 'executes those calls' do
|
137
|
+
assert_equal query.to_json, {
|
138
|
+
"123" => {
|
139
|
+
"comments" => {
|
140
|
+
"edges" => [
|
141
|
+
{
|
142
|
+
"cursor" => "445",
|
143
|
+
"node" => {
|
144
|
+
"content" => "I disagree"
|
145
|
+
}
|
146
|
+
}
|
147
|
+
]
|
148
|
+
}
|
149
|
+
}
|
150
|
+
}
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GraphQL::Transform do
|
4
|
+
let(:transform) { GraphQL::TRANSFORM }
|
5
|
+
let(:parser) { GraphQL::PARSER }
|
6
|
+
|
7
|
+
describe '#apply' do
|
8
|
+
it 'turns a simple node into a Node' do
|
9
|
+
tree = parser.parse("post(123) { name }")
|
10
|
+
res = transform.apply(tree)
|
11
|
+
assert(res.is_a?(GraphQL::Syntax::Node), 'it gets a node')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'turns a node into a Node' do
|
15
|
+
tree = parser.parse("viewer() { name, friends.first(10) { birthdate } }")
|
16
|
+
res = transform.apply(tree)
|
17
|
+
assert(res.is_a?(GraphQL::Syntax::Node), 'it gets a node')
|
18
|
+
assert(res.identifier == "viewer")
|
19
|
+
assert(res.fields.length == 2)
|
20
|
+
assert(res.fields[0].is_a?(GraphQL::Syntax::Field), 'it gets a field')
|
21
|
+
assert(res.fields[1].is_a?(GraphQL::Syntax::Edge), 'it gets an edge')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'turns a field into a Field' do
|
25
|
+
tree = parser.field.parse("friends")
|
26
|
+
res = transform.apply(tree)
|
27
|
+
assert(res.is_a?(GraphQL::Syntax::Field))
|
28
|
+
assert(res.identifier == "friends")
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'turns edge into an Edge' do
|
32
|
+
tree = parser.edge.parse("friends.after(123).first(2) { count, edges { node { name } } }")
|
33
|
+
res = transform.apply(tree)
|
34
|
+
assert(res.is_a?(GraphQL::Syntax::Edge), 'it gets the Edge')
|
35
|
+
assert(res.identifier == "friends")
|
36
|
+
assert(res.calls.length == 2, 'it tracks calls')
|
37
|
+
assert(res.calls[0].identifier == "after")
|
38
|
+
assert(res.calls[1].identifier == "first")
|
39
|
+
assert_equal(res.call_hash, {"after" => "123", "first" => "2"})
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'turns call into a Call' do
|
43
|
+
tree = parser.call.parse("node(123)")
|
44
|
+
res = transform.apply(tree)
|
45
|
+
assert(res.is_a?(GraphQL::Syntax::Call))
|
46
|
+
assert(res.identifier == "node")
|
47
|
+
assert(res.argument == "123")
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'turns a call without an argument into a Call' do
|
51
|
+
tree = parser.call.parse("viewer()")
|
52
|
+
res = transform.apply(tree)
|
53
|
+
assert(res.is_a?(GraphQL::Syntax::Call))
|
54
|
+
assert(res.identifier == "viewer")
|
55
|
+
assert(res.argument == nil)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
require "minitest/focus"
|
3
|
+
require "minitest/reporters"
|
4
|
+
|
5
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
6
|
+
|
7
|
+
# Filter out Minitest backtrace while allowing backtrace from other libraries
|
8
|
+
# to be shown.
|
9
|
+
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
|
10
|
+
|
11
|
+
# # Load support files
|
12
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
13
|
+
|
14
|
+
require 'parslet'
|
15
|
+
require 'parslet/convenience'
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class InadequateRecordBase
|
2
|
+
def initialize(attributes={})
|
3
|
+
attributes.each do |key, value|
|
4
|
+
self.send("#{key}=", value)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def destroy
|
9
|
+
self.class.objects.delete(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_accessor :_objects
|
14
|
+
def objects
|
15
|
+
@_objects ||= []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.find(id)
|
20
|
+
objects.find { |object| object.id.to_s == id.to_s}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.where(query={})
|
24
|
+
result = []
|
25
|
+
objects.each do |object|
|
26
|
+
match = true
|
27
|
+
|
28
|
+
query.each do |key, value|
|
29
|
+
if object.send(key) != value
|
30
|
+
match = false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
result << object if match
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.create(attributes)
|
40
|
+
instance = self.new(attributes)
|
41
|
+
objects << instance
|
42
|
+
instance
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Post < InadequateRecordBase
|
47
|
+
attr_accessor :id, :title, :content
|
48
|
+
|
49
|
+
def comments
|
50
|
+
Comment.where(post_id: id)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Comment < InadequateRecordBase
|
55
|
+
attr_accessor :id, :post_id, :content
|
56
|
+
def post
|
57
|
+
Post.find(post_id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Nodes
|
2
|
+
class PostNode < GraphQL::Node
|
3
|
+
field_reader :id, :title, :content
|
4
|
+
cursor :id
|
5
|
+
|
6
|
+
edges :comments,
|
7
|
+
collection_class_name: "Nodes::ApplicationCollectionEdge",
|
8
|
+
edge_class_name: "Nodes::CommentNode"
|
9
|
+
|
10
|
+
def teaser
|
11
|
+
content.length > 10 ? "#{content[0..9]}..." : content
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.call(argument)
|
15
|
+
post = Post.find(argument.to_i)
|
16
|
+
self.new(post)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class CommentNode < GraphQL::Node
|
21
|
+
field_reader :id, :post, :content
|
22
|
+
cursor :id
|
23
|
+
|
24
|
+
def self.call(argument)
|
25
|
+
obj = Comment.find(argument)
|
26
|
+
self.new(obj)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class ViewerNode < GraphQL::Node
|
31
|
+
def name
|
32
|
+
"It's you again"
|
33
|
+
end
|
34
|
+
|
35
|
+
def cursor
|
36
|
+
"viewer"
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.call(argument)
|
40
|
+
self.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class ApplicationCollectionEdge < GraphQL::CollectionEdge
|
45
|
+
def apply_calls(items, calls)
|
46
|
+
filtered_items = items
|
47
|
+
|
48
|
+
if calls["after"].present?
|
49
|
+
filtered_items = filtered_items.select {|i| i.id > calls["after"].to_i }
|
50
|
+
end
|
51
|
+
|
52
|
+
if calls["first"].present?
|
53
|
+
filtered_items = filtered_items.first(calls["first"].to_i)
|
54
|
+
end
|
55
|
+
|
56
|
+
filtered_items
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Mosolgo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: parslet
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.6.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.6.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard-bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest-focus
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest-reporters
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: A GraphQL adapter for Ruby
|
126
|
+
email:
|
127
|
+
- rdmosolgo@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- MIT-LICENSE
|
133
|
+
- lib/graphql.rb
|
134
|
+
- lib/graphql/collection_edge.rb
|
135
|
+
- lib/graphql/node.rb
|
136
|
+
- lib/graphql/parser.rb
|
137
|
+
- lib/graphql/query.rb
|
138
|
+
- lib/graphql/syntax/call.rb
|
139
|
+
- lib/graphql/syntax/edge.rb
|
140
|
+
- lib/graphql/syntax/field.rb
|
141
|
+
- lib/graphql/syntax/node.rb
|
142
|
+
- lib/graphql/transform.rb
|
143
|
+
- readme.md
|
144
|
+
- spec/graphql/parser_spec.rb
|
145
|
+
- spec/graphql/query_spec.rb
|
146
|
+
- spec/graphql/transform_spec.rb
|
147
|
+
- spec/spec_helper.rb
|
148
|
+
- spec/support/dummy_app.rb
|
149
|
+
- spec/support/nodes.rb
|
150
|
+
homepage: http://github.com/rmosolgo/graphql
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata: {}
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubyforge_project:
|
170
|
+
rubygems_version: 2.2.2
|
171
|
+
signing_key:
|
172
|
+
specification_version: 4
|
173
|
+
summary: GraphQL
|
174
|
+
test_files:
|
175
|
+
- spec/graphql/parser_spec.rb
|
176
|
+
- spec/graphql/query_spec.rb
|
177
|
+
- spec/graphql/transform_spec.rb
|
178
|
+
- spec/spec_helper.rb
|
179
|
+
- spec/support/dummy_app.rb
|
180
|
+
- spec/support/nodes.rb
|