graphql 0.0.3 → 0.0.4

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 (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