graphql 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +46 -29
  3. data/lib/graphql/call.rb +1 -1
  4. data/lib/graphql/connection.rb +12 -35
  5. data/lib/graphql/field.rb +7 -177
  6. data/lib/graphql/field_definer.rb +5 -15
  7. data/lib/graphql/introspection/{call_node.rb → call_type.rb} +1 -1
  8. data/lib/graphql/introspection/field_type.rb +10 -0
  9. data/lib/graphql/introspection/{root_call_node.rb → root_call_type.rb} +6 -2
  10. data/lib/graphql/introspection/schema_type.rb +17 -0
  11. data/lib/graphql/introspection/{type_node.rb → type_type.rb} +4 -2
  12. data/lib/graphql/node.rb +118 -42
  13. data/lib/graphql/{parser.rb → parser/parser.rb} +9 -4
  14. data/lib/graphql/{transform.rb → parser/transform.rb} +4 -2
  15. data/lib/graphql/query.rb +33 -10
  16. data/lib/graphql/root_call.rb +26 -13
  17. data/lib/graphql/root_call_argument.rb +3 -1
  18. data/lib/graphql/root_call_argument_definer.rb +3 -7
  19. data/lib/graphql/schema/all.rb +46 -0
  20. data/lib/graphql/{schema.rb → schema/schema.rb} +27 -39
  21. data/lib/graphql/schema/schema_validation.rb +32 -0
  22. data/lib/graphql/syntax/fragment.rb +7 -0
  23. data/lib/graphql/syntax/query.rb +3 -2
  24. data/lib/graphql/types/boolean_type.rb +3 -0
  25. data/lib/graphql/types/number_type.rb +3 -0
  26. data/lib/graphql/types/object_type.rb +6 -0
  27. data/lib/graphql/types/string_type.rb +3 -0
  28. data/lib/graphql/version.rb +1 -1
  29. data/readme.md +46 -5
  30. data/spec/graphql/node_spec.rb +6 -5
  31. data/spec/graphql/{parser_spec.rb → parser/parser_spec.rb} +31 -2
  32. data/spec/graphql/{transform_spec.rb → parser/transform_spec.rb} +16 -2
  33. data/spec/graphql/query_spec.rb +27 -9
  34. data/spec/graphql/root_call_spec.rb +15 -1
  35. data/spec/graphql/{schema_spec.rb → schema/schema_spec.rb} +15 -50
  36. data/spec/graphql/schema/schema_validation_spec.rb +48 -0
  37. data/spec/support/nodes.rb +31 -28
  38. metadata +47 -47
  39. data/lib/graphql/introspection/field_node.rb +0 -19
  40. data/lib/graphql/introspection/schema_node.rb +0 -17
  41. data/lib/graphql/types/boolean_field.rb +0 -3
  42. data/lib/graphql/types/connection_field.rb +0 -30
  43. data/lib/graphql/types/cursor_field.rb +0 -9
  44. data/lib/graphql/types/number_field.rb +0 -3
  45. data/lib/graphql/types/object_field.rb +0 -8
  46. data/lib/graphql/types/string_field.rb +0 -3
  47. data/lib/graphql/types/type_field.rb +0 -6
  48. data/spec/graphql/field_spec.rb +0 -63
@@ -1,9 +1,11 @@
1
1
  # Created by {RootCall.argument}, used internally by GraphQL
2
2
  class GraphQL::RootCallArgument
3
3
  attr_reader :type, :name, :any_number
4
- def initialize(type:, name:, any_number: false)
4
+ attr_accessor :index
5
+ def initialize(type:, name:, any_number: false, index: nil)
5
6
  @type = type
6
7
  @name = name
7
8
  @any_number = any_number
9
+ @index = index
8
10
  end
9
11
  end
@@ -2,20 +2,16 @@
2
2
  class GraphQL::RootCallArgumentDefiner
3
3
  ARGUMENT_TYPES = [:string, :object, :number]
4
4
 
5
- attr_reader :arguments
6
-
7
- def initialize(call_class)
8
- @call_class = call_class
9
- @arguments = []
5
+ def initialize(owner)
6
+ @owner = owner
10
7
  end
11
8
 
12
9
  def none
13
- @arguments = []
14
10
  end
15
11
 
16
12
  ARGUMENT_TYPES.each do |arg_type|
17
13
  define_method arg_type do |name, any_number: false|
18
- @arguments << GraphQL::RootCallArgument.new(type: arg_type.to_s, name: name.to_s, any_number: any_number)
14
+ @owner.add_argument(GraphQL::RootCallArgument.new(type: arg_type.to_s, name: name.to_s, any_number: any_number))
19
15
  end
20
16
  end
21
17
  end
@@ -0,0 +1,46 @@
1
+ # This query string yields the whole schema. Access it from {GraphQL::Schema::Schema#all}
2
+ GraphQL::Schema::ALL = "
3
+ schema() {
4
+ calls {
5
+ count,
6
+ edges {
7
+ node {
8
+ name,
9
+ returns,
10
+ arguments {
11
+ edges {
12
+ node {
13
+ name, type
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
19
+ },
20
+ types {
21
+ count,
22
+ edges {
23
+ node {
24
+ name,
25
+ fields {
26
+ count,
27
+ edges {
28
+ node {
29
+ name,
30
+ type,
31
+ calls {
32
+ count,
33
+ edges {
34
+ node {
35
+ name,
36
+ arguments
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }"
@@ -1,19 +1,36 @@
1
1
  # {GraphQL::SCHEMA} keeps track of defined nodes, fields and calls.
2
2
  #
3
3
  # Although you don't interact with it directly, it responds to queries for `schema()` and `__type__` info.
4
- class GraphQL::Schema
4
+ #
5
+ # You can validate it at runtime with {#validate}
6
+ # @example
7
+ # # validate the schema
8
+ # GraphQL::SCHEMA.validate
9
+ #
10
+ require "singleton"
11
+
12
+ class GraphQL::Schema::Schema
5
13
  include Singleton
6
- attr_reader :types, :calls, :fields, :class_names, :connections
14
+ attr_reader :types, :calls, :class_names
7
15
  def initialize
8
16
  @types = {}
9
- @connections = {}
10
- @fields = {}
11
17
  @class_names = {}
12
18
  @calls = {}
13
19
  end
14
20
 
21
+ # Queries the whole schema and returns the result
22
+ def all
23
+ GraphQL::Query.new(GraphQL::Schema::ALL).as_result
24
+ end
25
+
26
+ def validate
27
+ validation = GraphQL::Schema::SchemaValidation.new
28
+ validation.validate(self)
29
+ end
30
+
15
31
  def add_call(call_class)
16
32
  remove_call(call_class)
33
+ raise "You can't make #{call_class.name}'s type '#{call_class.schema_name}'" if call_class.schema_name.blank?
17
34
  @calls[call_class.schema_name] = call_class
18
35
  end
19
36
 
@@ -38,7 +55,10 @@ class GraphQL::Schema
38
55
  @types.delete(existing_name)
39
56
  end
40
57
 
41
- @class_names[node_class.ruby_class_name] = node_class
58
+ node_class.exposes_class_names.each do |exposes_class_name|
59
+ @class_names[exposes_class_name] = node_class
60
+ end
61
+
42
62
  @types[node_class.schema_name] = node_class
43
63
  end
44
64
 
@@ -47,7 +67,7 @@ class GraphQL::Schema
47
67
  end
48
68
 
49
69
  def type_names
50
- @types.keys
70
+ @types.keys.sort
51
71
  end
52
72
 
53
73
  def type_for_object(app_object)
@@ -62,38 +82,6 @@ class GraphQL::Schema
62
82
  return @class_names[class_name]
63
83
  end
64
84
  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
85
+ raise "Couldn't find node for class #{app_class} \"#{app_object}\" (ancestors: #{app_class.ancestors.map(&:name)}, defined: #{registered_class_names})"
98
86
  end
99
87
  end
@@ -0,0 +1,32 @@
1
+ # Validates a schema (specifically, {GraphQL::SCHEMA}).
2
+ #
3
+ # It checks:
4
+ # - All classes exposed by nodes actually exist
5
+ # - Field types requested by nodes actually exist
6
+ # - Fields' corresponding methods actually exist
7
+ #
8
+ # To validate a schema, use {GraphQL::Schema::Schema#validate}.
9
+ class GraphQL::Schema::SchemaValidation
10
+ # Validates the schema
11
+ def validate(schema)
12
+ schema.types.each do |type_name, type_class|
13
+
14
+ type_class.exposes_class_names.each do |exposes_class_name|
15
+ begin
16
+ Object.const_get(exposes_class_name)
17
+ rescue NameError
18
+ raise GraphQL::ExposesClassMissingError.new(type_class)
19
+ end
20
+ end
21
+
22
+ type_class.all_fields.each do |field_name, field_mapping|
23
+ # Make sure the type exists
24
+ field_mapping.type_class
25
+ # Make sure the node can handle it
26
+ if !type_class.respond_to_field?(field_mapping.name)
27
+ raise GraphQL::FieldNotDefinedError.new(type_class, field_mapping.name)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ class GraphQL::Syntax::Fragment
2
+ attr_reader :identifier, :fields
3
+ def initialize(identifier:, fields:)
4
+ @identifier = identifier
5
+ @fields = fields
6
+ end
7
+ end
@@ -1,7 +1,8 @@
1
1
  class GraphQL::Syntax::Query
2
- attr_reader :nodes, :variables
3
- def initialize(nodes:, variables:)
2
+ attr_reader :nodes, :variables, :fragments
3
+ def initialize(nodes:, variables:, fragments:)
4
4
  @nodes = nodes
5
5
  @variables = variables
6
+ @fragments = fragments
6
7
  end
7
8
  end
@@ -0,0 +1,3 @@
1
+ class GraphQL::Types::BooleanType < GraphQL::Types::ObjectType
2
+ exposes("TrueClass", "FalseClass")
3
+ end
@@ -0,0 +1,3 @@
1
+ class GraphQL::Types::NumberType < GraphQL::Types::ObjectType
2
+ exposes("Numeric")
3
+ end
@@ -0,0 +1,6 @@
1
+ class GraphQL::Types::ObjectType < GraphQL::Node
2
+ exposes("Object")
3
+ def as_result
4
+ target
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ class GraphQL::Types::StringType < GraphQL::Types::ObjectType
2
+ exposes("String")
3
+ end
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/readme.md CHANGED
@@ -5,7 +5,8 @@
5
5
  [![Dependency Status](https://gemnasium.com/rmosolgo/graphql-ruby.svg)](https://gemnasium.com/rmosolgo/graphql-ruby)
6
6
  [![Code Climate](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/gpa.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
7
7
  [![Test Coverage](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/coverage.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
8
- ![image](https://cloud.githubusercontent.com/assets/2231765/6424458/d5fd3896-beae-11e4-892a-77e135e6bf37.png)
8
+ [![built with love](https://cloud.githubusercontent.com/assets/2231765/6766607/d07992c6-cfc9-11e4-813f-d9240714dd50.png)](http://rmosolgo.github.io/react-badges/)
9
+
9
10
 
10
11
  Create a GraphQL interface by implementing [__nodes__](#nodes) and [__calls__](#calls), then running [__queries__](#queries).
11
12
 
@@ -13,14 +14,11 @@ API Docs: <http://rubydoc.info/gems/graphql>
13
14
 
14
15
  ## To do:
15
16
 
16
- - Allow a default connection class, or some way to infer connection from name
17
- - right now, `Introspection::Connection` isn't getting used, only `ApplicationConnection` is.
18
17
  - How do you express failure? HTTP response? `errors` key?
19
18
  - Handle blank objects in nested calls (how? wait for spec)
20
19
  - Implement calls as arguments
21
20
  - double-check how to handle `pals.first(3) { count }`
22
21
  - Implement call argument introspection (wait for spec)
23
- - For fields that return objects, can they be queried _without_ other fields? Or must they always have fields?
24
22
 
25
23
  ## Example Implementation
26
24
 
@@ -48,7 +46,8 @@ class FishNode < GraphQL::Node
48
46
  field.number(:id)
49
47
  field.string(:name)
50
48
  field.string(:species)
51
- field.object(:aquarium)
49
+ # specify an `AquariumNode`:
50
+ field.aquarium(:aquarium)
52
51
  end
53
52
  ```
54
53
 
@@ -64,6 +63,48 @@ class AquariumNode < GraphQL::Node
64
63
  end
65
64
  ```
66
65
 
66
+ You can make custom connections:
67
+
68
+ ```ruby
69
+ class FishSchoolConnection < GraphQL::Connection
70
+ type :fish_school # now it is a field type
71
+ call :largest, -> (prev_value, number) { fishes.sort_by(&:weight).first(number.to_i) }
72
+
73
+ field.number(:count) # delegated to `target`
74
+ field.boolean(:has_more)
75
+
76
+ def has_more
77
+ # the `largest()` call may have removed some items:
78
+ target.count < original_target.count
79
+ end
80
+ end
81
+ ```
82
+
83
+ Then use them:
84
+
85
+ ```ruby
86
+ class AquariumNode < GraphQL::Node
87
+ field.fish_school(:fishes)
88
+ end
89
+ ```
90
+
91
+ And in queries:
92
+
93
+ ```
94
+ aquarium(1) {
95
+ name,
96
+ occupancy,
97
+ fishes.largest(3) {
98
+ edges {
99
+ node { name, species }
100
+ },
101
+ count,
102
+ has_more
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
67
108
  ### Calls
68
109
 
69
110
  Calls selectively expose your application to the world. They always return values and they may perform mutations.
@@ -1,11 +1,12 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe GraphQL::Node do
4
- let(:query_string) { "type(post) { name, description, fields { count, edges { node { name, description }}} }"}
4
+ let(:query_string) { "type(post) { name, description, fields { count, edges { node { name, type }}} }"}
5
5
  let(:result) { GraphQL::Query.new(query_string).as_result}
6
6
 
7
7
  describe '__type__' do
8
8
  let(:title_field) { result["post"]["fields"]["edges"].find {|e| e["node"]["name"] == "title"}["node"] }
9
+
9
10
  it 'has name' do
10
11
  assert_equal "post", result["post"]["name"]
11
12
  end
@@ -16,7 +17,7 @@ describe GraphQL::Node do
16
17
 
17
18
  it 'has fields' do
18
19
  assert_equal 8, result["post"]["fields"]["count"]
19
- assert_equal({ "name" => "title", "description" => nil}, title_field)
20
+ assert_equal({ "name" => "title", "type" => "string"}, title_field)
20
21
  end
21
22
 
22
23
  describe 'getting the __type__ field' do
@@ -56,12 +57,12 @@ describe GraphQL::Node do
56
57
 
57
58
  describe 'type:' do
58
59
  it 'uses symbols to find built-ins' do
59
- id_field = Nodes::CommentNode.all_fields["id"]
60
- assert id_field.superclass == GraphQL::Types::NumberField
60
+ field_mapping = Nodes::CommentNode.all_fields["id"]
61
+ assert_equal GraphQL::Types::NumberType, field_mapping.type_class
61
62
  end
62
63
  it 'uses the provided class as a superclass' do
63
64
  letters_field = Nodes::CommentNode.all_fields["letters"]
64
- assert letters_field.superclass == Nodes::LetterSelectionField
65
+ assert_equal Nodes::LetterSelectionType, letters_field.type_class
65
66
  end
66
67
  end
67
68
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe GraphQL::Parser do
3
+ describe GraphQL::Parser::Parser do
4
4
  let(:parser) { GraphQL::PARSER }
5
5
 
6
6
  describe 'query' do
@@ -11,7 +11,7 @@ describe GraphQL::Parser do
11
11
  it 'parses node and variables' do
12
12
  assert query.parse_with_debug(%{
13
13
  like_page(<page>) {
14
- page { id }
14
+ $pageFragment
15
15
  }
16
16
  <page>: {
17
17
  "page": {"id": 1},
@@ -21,9 +21,14 @@ describe GraphQL::Parser do
21
21
  "page": {"id": 1},
22
22
  "person" : { "id", 4}
23
23
  }
24
+
25
+ $pageFragment: {
26
+ page { id }
27
+ }
24
28
  })
25
29
  end
26
30
  end
31
+
27
32
  describe 'field' do
28
33
  let(:field) { parser.field }
29
34
  it 'finds words' do
@@ -94,6 +99,10 @@ describe GraphQL::Parser do
94
99
  assert node.parse_with_debug("viewer() {id}")
95
100
  end
96
101
 
102
+ it 'parses query fragments' do
103
+ assert node.parse_with_debug("viewer() { id, $someFrag }")
104
+ end
105
+
97
106
  it 'parses nested nodes' do
98
107
  assert node.parse_with_debug("
99
108
  node(someone)
@@ -136,4 +145,24 @@ describe GraphQL::Parser do
136
145
  })
137
146
  end
138
147
  end
148
+
149
+ describe 'fragment' do
150
+ let(:fragment) { parser.fragment }
151
+
152
+ it 'gets parts of queries' do
153
+ assert fragment.parse_with_debug(%{ $frag: { id } })
154
+ end
155
+
156
+ it 'gets nested parts of queries' do
157
+ assert fragment.parse_with_debug(%{
158
+ $frag: {
159
+ item {
160
+ name,
161
+ price
162
+ },
163
+ $qtyFragment
164
+ }
165
+ })
166
+ end
167
+ end
139
168
  end