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,8 @@
1
+ class GraphQL::RootCallArgument
2
+ attr_reader :type, :name, :any_number
3
+ def initialize(type:, name:, any_number: false)
4
+ @type = type
5
+ @name = name
6
+ @any_number = any_number
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ class GraphQL::RootCallArgumentDefiner
2
+ ARGUMENT_TYPES = [:string, :object, :number]
3
+
4
+ attr_reader :arguments
5
+
6
+ def initialize(call_class)
7
+ @call_class = call_class
8
+ @arguments = []
9
+ end
10
+
11
+ def none
12
+ @arguments = []
13
+ end
14
+
15
+ ARGUMENT_TYPES.each do |arg_type|
16
+ define_method arg_type do |name, any_number: false|
17
+ @arguments << GraphQL::RootCallArgument.new(type: arg_type.to_s, name: name.to_s, any_number: any_number)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,99 @@
1
+ # {GraphQL::SCHEMA} keeps track of defined nodes, fields and calls.
2
+ # Although you don't interact with it directly, it responds to queries for `schema()` and `__type__` info.
3
+
4
+ class GraphQL::Schema
5
+ include Singleton
6
+ attr_reader :types, :calls, :fields, :class_names, :connections
7
+ def initialize
8
+ @types = {}
9
+ @connections = {}
10
+ @fields = {}
11
+ @class_names = {}
12
+ @calls = {}
13
+ end
14
+
15
+ def add_call(call_class)
16
+ remove_call(call_class)
17
+ @calls[call_class.schema_name] = call_class
18
+ end
19
+
20
+ def get_call(identifier)
21
+ @calls[identifier.to_s] || raise(GraphQL::RootCallNotDefinedError.new(identifier))
22
+ end
23
+
24
+ def remove_call(call_class)
25
+ existing_name = @calls.key(call_class)
26
+ if existing_name
27
+ @calls.delete(existing_name)
28
+ end
29
+ end
30
+
31
+ def call_names
32
+ @calls.keys
33
+ end
34
+
35
+ def add_type(node_class)
36
+ existing_name = @types.key(node_class)
37
+ if existing_name
38
+ @types.delete(existing_name)
39
+ end
40
+
41
+ @class_names[node_class.ruby_class_name] = node_class
42
+ @types[node_class.schema_name] = node_class
43
+ end
44
+
45
+ def get_type(identifier)
46
+ @types[identifier.to_s] || raise(GraphQL::NodeNotDefinedError.new(identifier))
47
+ end
48
+
49
+ def type_names
50
+ @types.keys
51
+ end
52
+
53
+ def type_for_object(app_object)
54
+ registered_class_names = @class_names.keys
55
+ if app_object.is_a?(Class)
56
+ app_class = app_object
57
+ else
58
+ app_class = app_object.class
59
+ end
60
+ app_class.ancestors.map(&:name).each do |class_name|
61
+ if registered_class_names.include?(class_name)
62
+ return @class_names[class_name]
63
+ end
64
+ end
65
+ raise "Couldn't find node for class #{app_class} #{app_object} (ancestors: #{app_class.ancestors.map(&:name)}, defined: #{registered_class_names})"
66
+ end
67
+
68
+ def add_connection(node_class)
69
+ existing_name = @connections.key(node_class)
70
+ if existing_name
71
+ @connections.delete(existing_name)
72
+ end
73
+ @connections[node_class.schema_name.to_s] = node_class
74
+ end
75
+
76
+ def get_connection(identifier)
77
+ @connections[identifier] || GraphQL::Connection.default_connection || raise(GraphQL::ConnectionNotDefinedError.new(identifier))
78
+ end
79
+
80
+ def connection_names
81
+ @connections.keys
82
+ end
83
+
84
+ def add_field(field_class)
85
+ existing_name = @fields.key(field_class)
86
+ if existing_name
87
+ @fields.delete(existing_name)
88
+ end
89
+ @fields[field_class.schema_name.to_s] = field_class
90
+ end
91
+
92
+ def get_field(identifier)
93
+ @fields[identifier.to_s] || raise(GraphQL::FieldNotDefinedError.new("<unknown>", identifier))
94
+ end
95
+
96
+ def field_names
97
+ @fields.keys
98
+ end
99
+ end
@@ -1,17 +1,8 @@
1
1
  class GraphQL::Syntax::Call
2
- attr_reader :identifier, :argument, :calls
3
- def initialize(identifier:, argument: nil, calls: [])
2
+ attr_reader :identifier, :arguments, :calls
3
+ def initialize(identifier:, arguments: nil, calls: [])
4
4
  @identifier = identifier
5
- @argument = argument
5
+ @arguments = arguments
6
6
  @calls = calls
7
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
8
  end
@@ -1,7 +1,9 @@
1
1
  class GraphQL::Syntax::Field
2
- attr_reader :identifier, :calls
3
- def initialize(identifier:, calls: [])
2
+ attr_reader :identifier, :alias_name, :calls, :fields
3
+ def initialize(identifier:, alias_name: nil, calls: [], fields: [])
4
4
  @identifier = identifier
5
+ @alias_name = alias_name
5
6
  @calls = calls
7
+ @fields = fields
6
8
  end
7
9
  end
@@ -1,15 +1,8 @@
1
1
  class GraphQL::Syntax::Node
2
- attr_reader :identifier, :argument, :fields
3
- def initialize(identifier:, argument:, fields: [])
2
+ attr_reader :identifier, :arguments, :fields
3
+ def initialize(identifier:, arguments:, fields: [])
4
4
  @identifier = identifier
5
- @argument = argument
5
+ @arguments = arguments
6
6
  @fields = fields
7
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
8
  end
@@ -0,0 +1,7 @@
1
+ class GraphQL::Syntax::Query
2
+ attr_reader :nodes, :variables
3
+ def initialize(nodes:, variables:)
4
+ @nodes = nodes
5
+ @variables = variables
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class GraphQL::Syntax::Variable
2
+ attr_reader :identifier, :json_string
3
+ def initialize(identifier:, json_string:)
4
+ @identifier = identifier
5
+ @json_string = json_string
6
+ end
7
+ end
@@ -1,10 +1,19 @@
1
1
  class GraphQL::Transform < Parslet::Transform
2
+ # query
3
+ rule(nodes: sequence(:n), variables: sequence(:v)) { GraphQL::Syntax::Query.new(nodes: n, variables: v)}
2
4
  # 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)}
5
+ rule(identifier: simple(:i), arguments: sequence(:a), fields: sequence(:f)) {GraphQL::Syntax::Node.new(identifier: i.to_s, arguments: a, fields: f)}
6
6
  # field
7
- rule(identifier: simple(:i)) { GraphQL::Syntax::Field.new(identifier: i.to_s)}
7
+ rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, fields: f, calls: c, alias_name: a.to_s)}
8
+ rule(identifier: simple(:i), calls: sequence(:c), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, calls: c, alias_name: a.to_s)}
9
+ rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f)) { GraphQL::Syntax::Field.new(identifier: i.to_s, fields: f, calls: c)}
10
+ rule(identifier: simple(:i), calls: sequence(:c)) { GraphQL::Syntax::Field.new(identifier: i.to_s, calls: c)}
11
+ rule(identifier: simple(:i), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, alias_name: a.to_s)}
8
12
  # call
9
- rule(identifier: simple(:i), argument: simple(:a)) { GraphQL::Syntax::Call.new(identifier: i.to_s, argument: a) }
13
+ rule(identifier: simple(:i), arguments: sequence(:a)) { GraphQL::Syntax::Call.new(identifier: i.to_s, arguments: a) }
14
+ # argument
15
+ rule(argument: simple(:a)) { a.to_s }
16
+ rule(identifier: simple(:i)) { i.to_s }
17
+ # variable
18
+ rule(identifier: simple(:i), json_string: simple(:j)) { GraphQL::Syntax::Variable.new(identifier: i.to_s, json_string: j.to_s)}
10
19
  end
@@ -0,0 +1,3 @@
1
+ class GraphQL::Types::BooleanField < GraphQL::Field
2
+ type :boolean
3
+ end
@@ -0,0 +1,30 @@
1
+ class GraphQL::Types::ConnectionField < GraphQL::Field
2
+ type "connection"
3
+
4
+ ["connection_class_name", "node_class_name"].each do |method_name|
5
+ define_method(method_name) do
6
+ const_get(method_name.upcase)
7
+ end
8
+ end
9
+
10
+ def connection_class
11
+ if connection_class_name.present?
12
+ Object.const_get(connection_class_name)
13
+ else
14
+ GraphQL::SCHEMA.get_connection(name)
15
+ end
16
+ end
17
+
18
+ def as_node
19
+ items = finished_value
20
+ connection_class.new(
21
+ items,
22
+ query: query,
23
+ fields: fields,
24
+ )
25
+ end
26
+
27
+ def as_result
28
+ as_node.as_result
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ class GraphQL::Types::CursorField
2
+ def initialize(value)
3
+ @value = value
4
+ end
5
+
6
+ def as_result
7
+ @value.to_s
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ class GraphQL::Types::NumberField < GraphQL::Field
2
+ type "number"
3
+ end
@@ -0,0 +1,8 @@
1
+ class GraphQL::Types::ObjectField < GraphQL::Field
2
+ type "object"
3
+ def as_result
4
+ node_class = GraphQL::SCHEMA.type_for_object(finished_value)
5
+ node = node_class.new(finished_value, query: query, fields: fields)
6
+ node.as_result
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ class GraphQL::Types::StringField < GraphQL::Field
2
+ type :string
3
+ end
@@ -0,0 +1,6 @@
1
+ class GraphQL::Types::TypeField < GraphQL::Types::ObjectField
2
+ type "__type__"
3
+ def finished_value
4
+ GraphQL::SCHEMA.get_type(self.owner.class.schema_name)
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module GraphQL
2
+ VERSION = "0.0.2"
3
+ end
data/readme.md CHANGED
@@ -1,18 +1,150 @@
1
1
  # graphql
2
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
3
+ [![Build Status](https://travis-ci.org/rmosolgo/graphql-ruby.svg?branch=master)](https://travis-ci.org/rmosolgo/graphql-ruby)
4
+ [![Gem Version](https://badge.fury.io/rb/graphql.svg)](https://rubygems.org/gems/graphql)
5
+ [![Dependency Status](https://gemnasium.com/rmosolgo/graphql-ruby.svg)](https://gemnasium.com/rmosolgo/graphql-ruby)
6
6
 
7
- See `/spec/support/dummy_app/nodes.rb` for node examples
8
7
 
9
- __Nodes__ provide information to queries by mapping to application objects (via `.call` and `field_reader`) or implementing fields themselves (eg `Nodes::PostNode#teaser`).
8
+ Create a GraphQL interface by implementing _nodes_ and _connections_, then running queries.
10
9
 
11
- __Edges__ handle node-to-node relationships.
10
+ ## To do:
12
11
 
13
12
 
14
- ## To do:
13
+ - Allow a default connection class, or some way to infer connection from name
14
+ - right now, `Introspection::Connection` isn't getting used, only `ApplicationConnection` is.
15
+
16
+ - How do you express failure? HTTP response? `errors` key?
17
+ - Handle blank objects in nested calls
18
+ - Implement calls as arguments
19
+ - double-check how to handle `pals.first(3) { count }`
20
+ - Implement call argument introspection (wait for spec)
21
+ - For fields that return objects, can they be queried _without_ other fields? Or must they always have fields?
22
+ - __document__ (wait for spec)
23
+
24
+ ## Example Implementation
25
+
26
+ - See test implementation in [`/spec/support/dummy_app/nodes.rb`](https://github.com/rmosolgo/graphql/blob/master/spec/support/nodes.rb)
27
+ - See `graphql-ruby-demo` with Rails on [github](https://github.com/rmosolgo/graphql-ruby-demo) or [heroku](http://graphql-ruby-demo.herokuapp.com/)
28
+
29
+ ![gql](https://cloud.githubusercontent.com/assets/2231765/6217972/5d24edda-b5ce-11e4-9e07-3548304af862.png)
30
+
31
+
32
+ ## Usage
33
+
34
+ - Implement _nodes_ that wrap objects in your application
35
+ - Implement _calls_ that return those objects (and may mutate the application state)
36
+ - Execute _queries_ and return the result.
37
+
38
+ ### Nodes
39
+
40
+ Nodes are delegators that wrap objects in your app. You must whitelist fields by declaring them in the class definition.
41
+
42
+
43
+ ```ruby
44
+ class FishNode < GraphQL::Node
45
+ exposes "Fish"
46
+ cursor(:id)
47
+ field.number(:id)
48
+ field.string(:name)
49
+ field.string(:species)
50
+ field.object(:aquarium)
51
+ end
52
+ ```
53
+
54
+ You can also declare connections between objects:
55
+
56
+ ```ruby
57
+ class AquariumNode < GraphQL::Node
58
+ exposes "Aquarium"
59
+ cursor(:id)
60
+ field.number(:id)
61
+ field.number(:occupancy)
62
+ field.connection(:fishes)
63
+ end
64
+ ```
65
+
66
+ ### Calls
67
+
68
+ Calls selectively expose your application to the world. They always return values and they may perform mutations.
69
+
70
+ Calls declare returns, declare arguments, and implement `#execute!`.
71
+
72
+ This call just finds values:
73
+
74
+ ```ruby
75
+ class FindFishCall < GraphQL::RootCall
76
+ returns :fish
77
+ argument.number(:id)
78
+ def execute!(id)
79
+ Fish.find(id)
80
+ end
81
+ end
82
+ ```
83
+
84
+ This call performs a mutation:
85
+
86
+ ```ruby
87
+ class RelocateFishCall < GraphQL::RootCall
88
+ returns :fish, :previous_aquarium, :new_aquarium
89
+ argument.number(:fish_id)
90
+ argument.number(:new_aquarium_id)
91
+
92
+ def execute!(fish_id, new_aquarium_id)
93
+ fish = Fish.find(fish_id)
94
+
95
+ # context is defined by the query, see below
96
+ if !context[:user].can_move?(fish)
97
+ raise RelocateNotAllowedError
98
+ end
99
+
100
+ previous_aquarium = fish.aquarium
101
+ new_aquarium = Aquarium.find(new_aquarium_id)
102
+ fish.update_attributes(aquarium: new_aquarium)
103
+ {
104
+ fish: fish,
105
+ previous_aquarium: previous_aquarium,
106
+ new_aquarium: new_aquarium,
107
+ }
108
+ end
109
+ end
110
+ ```
111
+
112
+ ### Queries
113
+
114
+ When your system is setup, you can perform queries from a string.
115
+
116
+ ```ruby
117
+ query_str = "find_fish(1) { name, species } "
118
+ query = GraphQL::Query.new(query_str)
119
+ result = query.as_result
120
+
121
+ result
122
+ # {
123
+ # "1" => {
124
+ # "name" => "Sharky",
125
+ # "species" => "Goldfish",
126
+ # }
127
+ # }
128
+ ```
129
+
130
+ Each query may also define a `context` object which will be accessible at every point in execution.
131
+
132
+ ```ruby
133
+ query_str = "move_fish(1, 3) { fish { name }, new_aquarium { occupancy } }"
134
+ query_ctx = {user: current_user, request: request}
135
+ query = GraphQL::Query.new(query_str, context: query_ctx)
136
+ result = query.as_result
137
+
138
+ result
139
+ # {
140
+ # "fish" => {
141
+ # "name" => "Sharky"
142
+ # },
143
+ # "new_aquarium" => {
144
+ # "occupancy" => 12
145
+ # }
146
+ # }
147
+ ```
148
+
149
+ You could do something like this [inside a Rails controller](https://github.com/rmosolgo/graphql-ruby-demo/blob/master/app/controllers/queries_controller.rb#L5).
15
150
 
16
- - Better class inference. Declaring edge classes is stupid.
17
- - How to authenticate?
18
- - What do graphql mutation queries even look like?