graphql 0.0.1 → 0.0.2

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.
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?