rails-graphql 1.0.0.beta → 1.0.0.rc2

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