rails-graphql 1.0.0.beta → 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/ext/gql_parser.c +1 -16
  3. data/ext/gql_parser.h +21 -0
  4. data/ext/shared.c +0 -5
  5. data/ext/shared.h +6 -6
  6. data/lib/generators/graphql/channel_generator.rb +27 -0
  7. data/lib/generators/graphql/controller_generator.rb +9 -4
  8. data/lib/generators/graphql/install_generator.rb +49 -0
  9. data/lib/generators/graphql/schema_generator.rb +9 -4
  10. data/lib/generators/graphql/templates/channel.erb +7 -0
  11. data/lib/generators/graphql/templates/config.rb +97 -0
  12. data/lib/generators/graphql/templates/controller.erb +2 -0
  13. data/lib/generators/graphql/templates/schema.erb +5 -3
  14. data/lib/gql_parser.so +0 -0
  15. data/lib/rails/graphql/alternative/field_set.rb +12 -0
  16. data/lib/rails/graphql/alternative/query.rb +13 -8
  17. data/lib/rails/graphql/alternative/subscription.rb +2 -1
  18. data/lib/rails/graphql/alternative.rb +4 -0
  19. data/lib/rails/graphql/argument.rb +5 -3
  20. data/lib/rails/graphql/callback.rb +10 -8
  21. data/lib/rails/graphql/collectors/hash_collector.rb +12 -1
  22. data/lib/rails/graphql/collectors/json_collector.rb +21 -0
  23. data/lib/rails/graphql/config.rb +86 -59
  24. data/lib/rails/graphql/directive/include_directive.rb +0 -1
  25. data/lib/rails/graphql/directive/skip_directive.rb +0 -1
  26. data/lib/rails/graphql/directive/specified_by_directive.rb +24 -0
  27. data/lib/rails/graphql/directive.rb +31 -25
  28. data/lib/rails/graphql/event.rb +7 -6
  29. data/lib/rails/graphql/field/authorized_field.rb +0 -5
  30. data/lib/rails/graphql/field/input_field.rb +0 -5
  31. data/lib/rails/graphql/field/mutation_field.rb +5 -6
  32. data/lib/rails/graphql/field/output_field.rb +13 -2
  33. data/lib/rails/graphql/field/proxied_field.rb +6 -6
  34. data/lib/rails/graphql/field/resolved_field.rb +1 -1
  35. data/lib/rails/graphql/field/subscription_field.rb +35 -52
  36. data/lib/rails/graphql/field/typed_field.rb +26 -2
  37. data/lib/rails/graphql/field.rb +20 -19
  38. data/lib/rails/graphql/global_id.rb +5 -1
  39. data/lib/rails/graphql/helpers/inherited_collection/array.rb +1 -0
  40. data/lib/rails/graphql/helpers/inherited_collection/base.rb +3 -1
  41. data/lib/rails/graphql/helpers/inherited_collection/hash.rb +2 -1
  42. data/lib/rails/graphql/helpers/registerable.rb +1 -1
  43. data/lib/rails/graphql/helpers/with_arguments.rb +3 -2
  44. data/lib/rails/graphql/helpers/with_assignment.rb +5 -5
  45. data/lib/rails/graphql/helpers/with_callbacks.rb +3 -3
  46. data/lib/rails/graphql/helpers/with_description.rb +10 -8
  47. data/lib/rails/graphql/helpers/with_directives.rb +5 -1
  48. data/lib/rails/graphql/helpers/with_events.rb +1 -0
  49. data/lib/rails/graphql/helpers/with_fields.rb +30 -24
  50. data/lib/rails/graphql/helpers/with_name.rb +3 -2
  51. data/lib/rails/graphql/helpers/with_schema_fields.rb +75 -51
  52. data/lib/rails/graphql/introspection.rb +1 -1
  53. data/lib/rails/graphql/railtie.rb +3 -2
  54. data/lib/rails/graphql/railties/app/base_channel.rb +10 -0
  55. data/lib/rails/graphql/railties/app/base_controller.rb +12 -0
  56. data/lib/rails/graphql/railties/app/views/_cable.js.erb +56 -0
  57. data/lib/rails/graphql/railties/app/views/_fetch.js.erb +20 -0
  58. data/lib/rails/graphql/railties/app/views/graphiql.html.erb +101 -0
  59. data/lib/rails/graphql/railties/base_generator.rb +3 -9
  60. data/lib/rails/graphql/railties/channel.rb +8 -8
  61. data/lib/rails/graphql/railties/controller.rb +51 -26
  62. data/lib/rails/graphql/request/arguments.rb +2 -1
  63. data/lib/rails/graphql/request/backtrace.rb +31 -10
  64. data/lib/rails/graphql/request/component/field.rb +15 -8
  65. data/lib/rails/graphql/request/component/fragment.rb +13 -7
  66. data/lib/rails/graphql/request/component/operation/subscription.rb +4 -6
  67. data/lib/rails/graphql/request/component/operation.rb +12 -5
  68. data/lib/rails/graphql/request/component/spread.rb +13 -4
  69. data/lib/rails/graphql/request/component/typename.rb +1 -1
  70. data/lib/rails/graphql/request/component.rb +2 -0
  71. data/lib/rails/graphql/request/context.rb +1 -1
  72. data/lib/rails/graphql/request/event.rb +6 -2
  73. data/lib/rails/graphql/request/helpers/directives.rb +1 -0
  74. data/lib/rails/graphql/request/helpers/selection_set.rb +10 -4
  75. data/lib/rails/graphql/request/helpers/value_writers.rb +8 -5
  76. data/lib/rails/graphql/request/prepared_data.rb +3 -1
  77. data/lib/rails/graphql/request/steps/organizable.rb +1 -1
  78. data/lib/rails/graphql/request/steps/preparable.rb +1 -1
  79. data/lib/rails/graphql/request/steps/resolvable.rb +1 -1
  80. data/lib/rails/graphql/request/strategy/sequenced_strategy.rb +3 -3
  81. data/lib/rails/graphql/request/strategy.rb +18 -4
  82. data/lib/rails/graphql/request/subscription.rb +18 -16
  83. data/lib/rails/graphql/request.rb +71 -41
  84. data/lib/rails/graphql/schema.rb +39 -86
  85. data/lib/rails/graphql/shortcuts.rb +11 -5
  86. data/lib/rails/graphql/source/active_record/builders.rb +22 -24
  87. data/lib/rails/graphql/source/active_record_source.rb +96 -34
  88. data/lib/rails/graphql/source/base.rb +13 -40
  89. data/lib/rails/graphql/source/builder.rb +14 -22
  90. data/lib/rails/graphql/source/scoped_arguments.rb +10 -4
  91. data/lib/rails/graphql/source.rb +31 -38
  92. data/lib/rails/graphql/subscription/provider/action_cable.rb +10 -9
  93. data/lib/rails/graphql/subscription/provider/base.rb +6 -5
  94. data/lib/rails/graphql/subscription/store/base.rb +5 -9
  95. data/lib/rails/graphql/subscription/store/memory.rb +18 -9
  96. data/lib/rails/graphql/type/creator.rb +198 -0
  97. data/lib/rails/graphql/type/enum.rb +17 -9
  98. data/lib/rails/graphql/type/input.rb +30 -7
  99. data/lib/rails/graphql/type/interface.rb +15 -4
  100. data/lib/rails/graphql/type/object/directive_object.rb +6 -5
  101. data/lib/rails/graphql/type/object/input_value_object.rb +3 -4
  102. data/lib/rails/graphql/type/object/type_object.rb +40 -13
  103. data/lib/rails/graphql/type/object.rb +11 -6
  104. data/lib/rails/graphql/type/scalar/binary_scalar.rb +2 -0
  105. data/lib/rails/graphql/type/scalar/date_scalar.rb +2 -0
  106. data/lib/rails/graphql/type/scalar/date_time_scalar.rb +2 -0
  107. data/lib/rails/graphql/type/scalar/decimal_scalar.rb +2 -0
  108. data/lib/rails/graphql/type/scalar/json_scalar.rb +3 -1
  109. data/lib/rails/graphql/type/scalar/time_scalar.rb +3 -1
  110. data/lib/rails/graphql/type/scalar.rb +2 -2
  111. data/lib/rails/graphql/type/union.rb +7 -2
  112. data/lib/rails/graphql/type.rb +10 -2
  113. data/lib/rails/graphql/type_map.rb +20 -7
  114. data/lib/rails/graphql/uri.rb +5 -4
  115. data/lib/rails/graphql/version.rb +6 -2
  116. data/lib/rails/graphql.rb +11 -8
  117. data/test/assets/introspection-mem.txt +1 -1
  118. data/test/assets/introspection.gql +2 -0
  119. data/test/assets/mem.gql +74 -60
  120. data/test/assets/mysql.gql +69 -55
  121. data/test/assets/sqlite.gql +78 -64
  122. data/test/assets/translate.gql +50 -39
  123. data/test/config.rb +2 -1
  124. data/test/graphql/schema_test.rb +2 -31
  125. data/test/graphql/source_test.rb +1 -11
  126. data/test/graphql/type/interface_test.rb +8 -5
  127. data/test/graphql/type/object_test.rb +8 -2
  128. data/test/graphql/type_map_test.rb +13 -16
  129. data/test/integration/global_id_test.rb +4 -4
  130. data/test/integration/memory/star_wars_validation_test.rb +2 -2
  131. data/test/integration/mysql/star_wars_introspection_test.rb +1 -1
  132. data/test/integration/resolver_precedence_test.rb +1 -1
  133. data/test/integration/schemas/memory.rb +3 -4
  134. data/test/integration/sqlite/star_wars_global_id_test.rb +27 -21
  135. data/test/integration/sqlite/star_wars_introspection_test.rb +1 -1
  136. data/test/integration/translate_test.rb +26 -14
  137. metadata +22 -9
@@ -65,6 +65,11 @@ module Rails
65
65
  raise NotImplementedError, +"#{self.class.name} does not implement remove"
66
66
  end
67
67
 
68
+ # Marks that a subscription has received an update
69
+ def update!(item)
70
+ raise NotImplementedError, +"#{self.class.name} does not implement update!"
71
+ end
72
+
68
73
  # Check if a given sid or instance is stored
69
74
  def has?(item)
70
75
  raise NotImplementedError, +"#{self.class.name} does not implement has?"
@@ -123,21 +128,12 @@ module Rails
123
128
  def hash_for(value, klass = nil)
124
129
  if !klass.nil?
125
130
  klass.hash ^ value.hash
126
- elsif extract_class_from?(value)
127
- value.class.hash ^ value.id.hash
128
131
  elsif value.is_a?(Numeric)
129
132
  value
130
133
  else
131
134
  value.hash
132
135
  end
133
136
  end
134
-
135
- # Check if ActiveRecord::Base is available and then if the object
136
- # provided is an instance of it, so that the serialize can work
137
- # correctly
138
- def extract_class_from?(value)
139
- defined?(ActiveRecord) && value.is_a?(ActiveRecord::Base)
140
- end
141
137
  end
142
138
  end
143
139
  end
@@ -49,6 +49,11 @@ module Rails
49
49
  raise ::ArgumentError, +"SID #{subscription.sid} is already taken."
50
50
  end
51
51
 
52
+ # Rewrite the scope, to save memory
53
+ scope = possible_scopes(subscription.scope)&.first
54
+ subscription.instance_variable_set(:@scope, scope)
55
+
56
+ # Save to the list and to the index
52
57
  list[subscription.sid] = subscription
53
58
  index_set = subscription_to_index(subscription).reduce(index, &:[])
54
59
  index_set << subscription.sid
@@ -56,13 +61,13 @@ module Rails
56
61
  end
57
62
 
58
63
  def fetch(*sids)
59
- if sids.none?
60
- nil
61
- elsif sids.one?
62
- list[sids.first]
63
- else
64
- sids.map(&list.method(:[]))
64
+ return if sids.none?
65
+
66
+ items = sids.map do |item|
67
+ instance?(item) ? item : list[item]
65
68
  end
69
+
70
+ items.one? ? items.first : items
66
71
  end
67
72
 
68
73
  def remove(item)
@@ -82,6 +87,10 @@ module Rails
82
87
  index.delete(path[0]) if f_level.empty?
83
88
  end
84
89
 
90
+ def update!(item)
91
+ (instance?(item) ? item : fetch(item)).update!
92
+ end
93
+
85
94
  def has?(item)
86
95
  list.key?(instance?(item) ? item.sid : item)
87
96
  end
@@ -115,9 +124,9 @@ module Rails
115
124
  # Turn the request subscription into into the path of the index
116
125
  def subscription_to_index(subscription)
117
126
  [
118
- hash_for(subscription.field),
119
- possible_scopes(subscription.scope)&.first,
120
- hash_for(subscription.args),
127
+ subscription.field.hash,
128
+ subscription.scope,
129
+ subscription.args.hash,
121
130
  ]
122
131
  end
123
132
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module GraphQL
5
+ class Type
6
+ # = GraphQL Type Creator
7
+ #
8
+ # This class helps to dynamically create types using a large set of
9
+ # settings that are provided through the named arguments. There are
10
+ # setting that are general to all types, and some are specific per base
11
+ # type of the superclass
12
+ class Creator
13
+ NESTED_MODULE = :NestedTypes
14
+ SUPPORTED_KINDS = %i[scalar object interface union enum input_object source].freeze
15
+
16
+ attr_reader :name, :superclass, :klass, :settings
17
+
18
+ delegate :type_map, to: '::Rails::GraphQL'
19
+ delegate :kind, to: :superclass
20
+
21
+ # Simply instantiate the creator and run the process
22
+ def self.create!(*args, **xargs, &block)
23
+ new(*args, **xargs).create!(&block)
24
+ end
25
+
26
+ def initialize(from, name_or_object, superclass, **settings)
27
+ @from = from
28
+ @settings = settings
29
+ @object = name_or_object if name_or_object.is_a?(Class)
30
+
31
+ @superclass = sanitize_superclass(superclass)
32
+ @name = sanitize_name(name_or_object)
33
+ end
34
+
35
+ # Go over the create process
36
+ def create!(&block)
37
+ @klass = find_or_create_class
38
+ klass.instance_variable_set(:@gql_name, gql_name)
39
+
40
+ apply_general_settings
41
+ after_block = apply_specific_settings
42
+
43
+ klass.module_exec(&block) if block.present?
44
+ after_block.call if after_block.is_a?(Proc)
45
+ klass
46
+ end
47
+
48
+ protected
49
+
50
+ # Use the type map to look for a type from the same namespace
51
+ def find_type!(value)
52
+ type_map.fetch!(value, namespaces: namespaces, base_class: :Type)
53
+ end
54
+
55
+ # Same as above, but mapping the list
56
+ def find_all_types!(*list)
57
+ list.map { |item| item.is_a?(Class) ? item : find_type!(item) }
58
+ end
59
+
60
+ # Apply settings that is common for any possible type created
61
+ def apply_general_settings
62
+ klass.abstract = settings[:abstract] if settings.key?(:abstract)
63
+ klass.set_namespace(*namespaces)
64
+
65
+ klass.use(*settings[:directives]) if settings.key?(:directives) &&
66
+ klass.is_a?(Helpers::WithDirectives)
67
+
68
+ if klass.is_a?(Helpers::WithAssignment)
69
+ assignment = settings.fetch(:assigned_to, @object)
70
+ klass.assigned_to = assignment unless assignment.nil?
71
+ end
72
+
73
+ if settings.key?(:owner)
74
+ klass.include(Helpers::WithOwner) unless klass.is_a?(Helpers::WithOwner)
75
+ klass.owner = settings[:owner] == true ? @from : settings[:owner]
76
+ end
77
+ end
78
+
79
+ # Using the kind, call of a specific method to further configure the
80
+ # created class
81
+ def apply_specific_settings
82
+ method_name = +"apply_#{kind}_settings"
83
+ send(method_name) if respond_to?(method_name, true)
84
+ end
85
+
86
+ # Specific settings when creating an enum
87
+ def apply_enum_settings
88
+ klass.indexed! if settings[:indexed]
89
+ return if (values = settings[:values]).nil?
90
+ GraphQL.enumerate(values).each(&klass.method(:add))
91
+ end
92
+
93
+ # Specific settings when creating an union
94
+ def apply_union_settings
95
+ types = settings[:of_types]
96
+ klass.append(*find_all_types!(*types)) unless types.nil?
97
+ end
98
+
99
+ # Specific settings when creating a source
100
+ def apply_source_settings
101
+ build = settings[:build]
102
+
103
+ -> do
104
+ return klass.build_all if build == true
105
+ GraphQL.enumerate(build).each do |step|
106
+ klass.public_send(+"build_#{step}")
107
+ end
108
+ end if build
109
+ end
110
+
111
+ private
112
+
113
+ # Either get the gql name from settings or properly resolve one from
114
+ # the name
115
+ def gql_name
116
+ settings[:gql_name].presence || begin
117
+ gql_name = name.dup
118
+ gql_name = gql_name.chomp(name_suffix) unless kind == :input_object
119
+ gql_name.tr('_', '')
120
+ end
121
+ end
122
+
123
+ # Add the nested module to the source of the creating and create the
124
+ # class. If any of those exist, return the constant instead
125
+ def find_or_create_class
126
+ base = base_module
127
+
128
+ # Create the class under the nested module
129
+ return base.const_set(name, Class.new(superclass)) \
130
+ unless base.const_defined?(name, false)
131
+
132
+ # Get the existing class and check for the once setting
133
+ klass = base.const_get(name, false)
134
+ return klass unless !once? && klass < superclass
135
+
136
+ # Created once or not from the same superclass
137
+ raise DuplicatedError, (+<<~MSG).squish
138
+ A type named "#{name}" already exists for the "#{base.name}" module.
139
+ MSG
140
+ end
141
+
142
+ # Make sure to properly get the superclass
143
+ def sanitize_superclass(value)
144
+ value = Type.const_get(value.to_s.classify, false) unless value.is_a?(Class)
145
+
146
+ valid_class = value.is_a?(Class) && value.respond_to?(:kind)
147
+ valid_class &= SUPPORTED_KINDS.include?(value.kind)
148
+ return value if valid_class
149
+
150
+ raise ::ArgumentError, +"The \"#{value.inspect}\" is not a valid superclass."
151
+ rescue ::NameError
152
+ raise ::ArgumentError, +"Unable to find a \"#{value}\" superclass."
153
+ end
154
+
155
+ # Let's clean up the name
156
+ def sanitize_name(name_or_object)
157
+ name = name_or_object.is_a?(Module) ? name_or_object.name : name_or_object.to_s
158
+ name = name.classify.delete_prefix('GraphQL::').gsub(/::/, '_')
159
+ name.end_with?(name_suffix) ? name : name + name_suffix
160
+ end
161
+
162
+ # Figure out the suffix that is supposed to be used for the name
163
+ def name_suffix
164
+ @name_suffix ||= @settings[:suffix] || begin
165
+ if kind == :input_object
166
+ GraphQL.config.auto_suffix_input_objects
167
+ else
168
+ superclass.kind.to_s.classify
169
+ end
170
+ end
171
+ end
172
+
173
+ # Get or set the base module using the from argument
174
+ def base_module
175
+ base = @from.is_a?(Module) ? @from : @from.class
176
+ if base.const_defined?(NESTED_MODULE, false)
177
+ base.const_get(NESTED_MODULE, false)
178
+ else
179
+ base.const_set(NESTED_MODULE, Module.new).tap do
180
+ base.private_constant(NESTED_MODULE)
181
+ end
182
+ end
183
+ end
184
+
185
+ # Get the namespaces from the settings or from the source
186
+ def namespaces
187
+ @namespaces ||= settings[:namespace] || settings[:namespaces] || @from.namespace
188
+ end
189
+
190
+ # Check if the type should be create only once, meaning that returning
191
+ # an existing one is not an option
192
+ def once?
193
+ settings.fetch(:once, true)
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -75,6 +75,7 @@ module Rails
75
75
  # ==== Options
76
76
  #
77
77
  # * <tt>:desc</tt> - The description of the enum value (defaults to nil).
78
+ # * <tt>:description</tt> - Alias to the above.
78
79
  # * <tt>:directives</tt> - The list of directives associated with the value
79
80
  # (defaults to nil).
80
81
  # * <tt>:deprecated</tt> - A shortcut that auto-attach a @deprecated
@@ -82,9 +83,9 @@ module Rails
82
83
  # but provide a string so it can be used as the reason of the deprecation.
83
84
  # See {DeprecatedDirective}[rdoc-ref:Rails::GraphQL::Directive::DeprecatedDirective]
84
85
  # (defaults to false).
85
- def add(value, desc: nil, directives: nil, deprecated: false)
86
+ def add(value, desc: nil, description: nil, directives: nil, deprecated: false)
86
87
  value = value&.to_s
87
- raise ArgumentError, (+<<~MSG).squish unless value.is_a?(String) && value.present?
88
+ raise ArgumentError, (+<<~MSG).squish unless value.is_a?(String) && !value.empty?
88
89
  The "#{value}" is invalid.
89
90
  MSG
90
91
 
@@ -103,6 +104,8 @@ module Rails
103
104
  source: self,
104
105
  )
105
106
 
107
+ desc = description if desc.nil?
108
+
106
109
  values << value
107
110
  value_description[value] = desc unless desc.nil?
108
111
  value_directives[value] = directives if directives
@@ -130,10 +133,12 @@ module Rails
130
133
 
131
134
  def inspect
132
135
  return super if self.eql?(Type::Enum)
136
+
137
+ values = all_values.to_a
133
138
  (+<<~INFO).squish << '>'
134
139
  #<GraphQL::Enum #{gql_name}
135
- (#{all_values.size})
136
- {#{all_values.to_a.join(' | ')}}
140
+ (#{values.size})
141
+ {#{values.to_a.join(' | ')}}
137
142
  #{inspect_directives}
138
143
  INFO
139
144
  end
@@ -143,6 +148,8 @@ module Rails
143
148
 
144
149
  delegate :to_s, :inspect, to: :@value
145
150
 
151
+ # TODO: Maybe add delegate missing
152
+
146
153
  def initialize(value)
147
154
  @value = value
148
155
  end
@@ -165,13 +172,15 @@ module Rails
165
172
  # Gets all the description of the current value
166
173
  def description
167
174
  return unless @value
168
- @description ||= all_value_description.try(:[], @value)
175
+ return @description if defined?(@description)
176
+ @description = all_value_description.try(:[], @value)
169
177
  end
170
178
 
171
179
  # Gets all the directives associated with the current value
172
180
  def directives
173
181
  return unless @value
174
- @directives ||= all_value_directives.try(:[], @value)
182
+ return @directives if defined?(@directives)
183
+ @directives = all_value_directives.try(:[], @value)
175
184
  end
176
185
 
177
186
  # Checks if the current value is marked as deprecated
@@ -181,10 +190,9 @@ module Rails
181
190
 
182
191
  # Return the deprecated reason
183
192
  def deprecated_reason
184
- return unless deprecated?
185
- directives.find do |dir|
193
+ directives&.find do |dir|
186
194
  dir.is_a?(Directive::DeprecatedDirective)
187
- end.args.reason
195
+ end&.args&.reason
188
196
  end
189
197
  end
190
198
  end
@@ -105,7 +105,7 @@ module Rails
105
105
  end
106
106
  end
107
107
 
108
- attr_reader :args
108
+ attr_reader :args, :assignment_error
109
109
  attr_writer :resource
110
110
 
111
111
  delegate :fields, to: :class
@@ -114,7 +114,7 @@ module Rails
114
114
  delegate_missing_to :resource
115
115
 
116
116
  def initialize(args = nil, **xargs)
117
- @args = args || OpenStruct.new(xargs.transform_keys { |key| key.to_s.underscore })
117
+ @args = args || build_ostruct(xargs)
118
118
  @args.freeze
119
119
 
120
120
  validate! if args.nil?
@@ -124,7 +124,10 @@ module Rails
124
124
  # received arguments. It also accepts extra arguments for inheritance
125
125
  # purposes
126
126
  def resource(*args, **xargs, &block)
127
- @resource ||= (klass = safe_assigned_class).nil? ? nil : begin
127
+ return @resource if defined?(@resource)
128
+ return if (klass = safe_assigned_class).nil?
129
+
130
+ @resource = begin
128
131
  xargs = xargs.reverse_merge(params)
129
132
  klass.new(*args, **xargs, &block)
130
133
  end
@@ -132,21 +135,25 @@ module Rails
132
135
 
133
136
  # Just return the arguments as an hash
134
137
  def params
135
- parametrize(self)
138
+ parametrize(@args.to_h)
136
139
  end
137
140
 
138
- # Corretly turn all the arguments into their +as_json+ version and
141
+ # Correctly turn all the arguments into their +as_json+ version and
139
142
  # return a hash of them
140
143
  def args_as_json
141
144
  self.class.as_json(@args.to_h)
142
145
  end
143
146
 
144
- # Corretly turn all the arguments into their +to_json+ version and
147
+ alias as_json args_as_json
148
+
149
+ # Correctly turn all the arguments into their +to_json+ version and
145
150
  # return a hash of them
146
151
  def args_to_json
147
152
  self.class.to_json(@args.to_h)
148
153
  end
149
154
 
155
+ alias to_json args_to_json
156
+
150
157
  # Checks if all the values provided to the input instance are valid
151
158
  def validate!(*)
152
159
  errors = []
@@ -162,18 +169,34 @@ module Rails
162
169
  MSG
163
170
  end
164
171
 
172
+ # Override this method to save any errors that could happen with loading
173
+ # the assigned class
174
+ def safe_assigned_class
175
+ assigned_class
176
+ rescue ::ArgumentError, ::NameError => error
177
+ @assignment_error = error
178
+ nil
179
+ end
180
+
165
181
  %i[to_global_id to_gid to_gid_param].each do |method_name|
166
182
  define_method(method_name) do
167
183
  self.class.public_send(method_name, args_as_json.compact)
168
184
  end
169
185
  end
170
186
 
187
+ protected
188
+
189
+ # A helper to turn a hash into a proper Open Struct instance
190
+ def build_ostruct(hash)
191
+ OpenStruct.new(hash.transform_keys { |key| key.to_s.underscore })
192
+ end
193
+
171
194
  private
172
195
 
173
196
  # Make sure to turn inputs into params
174
197
  def parametrize(input)
175
198
  case input
176
- when Type::Input then parametrize(input.args.to_h)
199
+ when Type::Input then input.params
177
200
  when Array then input.map(&method(:parametrize))
178
201
  when Hash then input.transform_values(&method(:parametrize))
179
202
  else input
@@ -26,6 +26,11 @@ module Rails
26
26
  inherited_collection :types, instance_reader: false
27
27
 
28
28
  class << self
29
+ # Figure out which one of the types is compatible with the provided +value+
30
+ def type_for(value, *)
31
+ all_types&.reverse_each&.find { |t| t.valid_member?(value) }
32
+ end
33
+
29
34
  # Check if the other type is equivalent, by checking if the other is
30
35
  # an object and the object implements this interface
31
36
  def =~(other)
@@ -35,16 +40,22 @@ module Rails
35
40
  # When attaching an interface to an object, copy the fields and add to
36
41
  # the list of types. Pre-existing same-named fields with are not
37
42
  # equivalent produces an exception.
38
- def implemented(object)
43
+ def implemented(object, import_fields: true)
44
+ import_fields = false if abstract?
45
+
39
46
  fields.each do |name, field|
40
- defined = object.field?(name)
41
- invalid = defined && object.fields[name] !~ field
47
+ defined = object[field.name]
48
+ raise ArgumentError, (+<<~MSG).squish unless defined || import_fields
49
+ The "#{object.gql_name}" object must have a "#{field.gql_name}" field.
50
+ MSG
51
+
52
+ invalid = defined && defined !~ field
42
53
  raise ArgumentError, (+<<~MSG).squish if invalid
43
54
  The "#{object.gql_name}" object already has a "#{field.gql_name}" field and it
44
55
  is not equivalent to the one defined on the "#{gql_name}" interface.
45
56
  MSG
46
57
 
47
- object.proxy_field(field) unless defined
58
+ object.proxy_field(field) if import_fields && !defined
48
59
  end
49
60
 
50
61
  types << object
@@ -20,13 +20,14 @@ module Rails
20
20
  additional information to the executor.
21
21
  DESC
22
22
 
23
- field :name, :string, null: false, method_name: :gql_name
24
- field :description, :string
25
- field :locations, '__DirectiveLocation', full: true
26
- field :args, '__InputValue', full: true
23
+ field :name, :string, null: false, method_name: :gql_name
24
+ field :description, :string
25
+ field :locations, '__DirectiveLocation', full: true
26
+ field :args, '__InputValue', full: true
27
+ field :is_repeatable, :boolean, null: false, method_name: :repeatable?
27
28
 
28
29
  def args
29
- all_arguments.values
30
+ all_arguments.each_value
30
31
  end
31
32
  end
32
33
  end
@@ -17,10 +17,9 @@ module Rails
17
17
  rename! '__InputValue'
18
18
 
19
19
  desc <<~DESC
20
- Alongside with scalars and enums, input value objects allow the user
21
- to provide values to arguments on fields and directives. Different
22
- from those, input values accepts a list of keyed values, instead of
23
- a single value.
20
+ Arguments provided to Fields or Directives and the input fields of an
21
+ InputObject are represented as Input Values which describe their type
22
+ and optionally a default value.
24
23
  DESC
25
24
 
26
25
  field :name, :string, null: false, method_name: :gql_name
@@ -12,15 +12,19 @@ module Rails
12
12
  kind: :list,
13
13
  kind_enum: 'LIST',
14
14
  name: 'List',
15
+ gql_name: nil,
15
16
  object?: true,
16
17
  description: nil,
18
+ of_type: nil,
17
19
  },
18
20
  non_null: {
19
21
  kind: :non_null,
20
22
  kind_enum: 'NON_NULL',
21
23
  name: 'Non-Null',
24
+ gql_name: nil,
22
25
  object?: true,
23
26
  description: nil,
27
+ of_type: nil,
24
28
  },
25
29
  }.freeze
26
30
 
@@ -32,7 +36,7 @@ module Rails
32
36
  end
33
37
 
34
38
  def self.fake_type_object(type, subtype)
35
- OpenStruct.new(**FAKE_TYPES[type].merge(of_type: subtype))
39
+ FAKE_TYPES[type].merge(of_type: subtype)
36
40
  end
37
41
 
38
42
  rename! '__Type'
@@ -59,13 +63,15 @@ module Rails
59
63
  the schema to define exactly what data is expected.
60
64
  DESC
61
65
 
62
- field :kind, '__TypeKind', null: false,
66
+ field :kind, '__TypeKind', null: false,
63
67
  method_name: :kind_enum
64
68
 
65
- field :name, :string,
69
+ field :name, :string,
66
70
  method_name: :gql_name
67
71
 
68
- field :description, :string
72
+ field :description, :string
73
+
74
+ field :specified_by_url, :string
69
75
 
70
76
  field :fields, '__Field', array: true, nullable: false do
71
77
  desc 'OBJECT and INTERFACE only'
@@ -89,8 +95,16 @@ module Rails
89
95
  field :of_type, '__Type',
90
96
  desc: 'NON_NULL and LIST only'
91
97
 
98
+ def specified_by_url
99
+ return if fake_type? || !current.scalar?
100
+
101
+ current.all_directives&.find do |dir|
102
+ dir.is_a?(Directive::SpecifiedByDirective)
103
+ end&.args&.url
104
+ end
105
+
92
106
  def fields(include_deprecated:)
93
- return EMPTY_ARRAY unless current.object? || current.interface?
107
+ return if fake_type? || !(current.object? || current.interface?)
94
108
 
95
109
  list =
96
110
  if current.respond_to?(:enabled_fields)
@@ -109,7 +123,7 @@ module Rails
109
123
  end
110
124
 
111
125
  def enum_values(include_deprecated:)
112
- return EMPTY_ARRAY unless current.enum?
126
+ return if fake_type? || !current.enum?
113
127
 
114
128
  descriptions = all_value_description
115
129
  deprecated = all_deprecated_values
@@ -119,26 +133,39 @@ module Rails
119
133
  unless include_deprecated || deprecated.nil?
120
134
 
121
135
  list.map do |value|
122
- OpenStruct.new(
136
+ {
123
137
  name: value,
124
- description: descriptions[value],
138
+ description: descriptions.try(:[], value),
125
139
  is_deprecated: (deprecated.nil? ? false : deprecated.key?(value)),
126
140
  deprecation_reason: deprecated.try(:[], value),
127
- )
141
+ }
128
142
  end
129
143
  end
130
144
 
131
145
  def interfaces
132
- (current.object? && current.all_interfaces) || EMPTY_ARRAY
146
+ return if fake_type? || !current.object?
147
+ current.all_interfaces || EMPTY_ARRAY
133
148
  end
134
149
 
135
150
  def possible_types
136
- (current.interface? && current.all_types) ||
137
- (current.union? && current.all_members) || EMPTY_ARRAY
151
+ return if fake_type?
152
+
153
+ if current.interface?
154
+ current.all_types || EMPTY_ARRAY
155
+ elsif current.union?
156
+ current.all_members || EMPTY_ARRAY
157
+ end
138
158
  end
139
159
 
140
160
  def input_fields
141
- (current.input? && current.enabled_fields) || EMPTY_ARRAY
161
+ return if fake_type? || !current.input?
162
+ current.enabled_fields || EMPTY_ARRAY
163
+ end
164
+
165
+ private
166
+
167
+ def fake_type?
168
+ Hash === current
142
169
  end
143
170
  end
144
171
  end