graphql 1.8.0.pre2 → 1.8.0.pre3

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +1 -1
  3. data/lib/graphql/deprecated_dsl.rb +2 -0
  4. data/lib/graphql/enum_type.rb +1 -1
  5. data/lib/graphql/field.rb +10 -1
  6. data/lib/graphql/input_object_type.rb +3 -1
  7. data/lib/graphql/introspection.rb +3 -10
  8. data/lib/graphql/introspection/base_object.rb +15 -0
  9. data/lib/graphql/introspection/directive_location_enum.rb +11 -7
  10. data/lib/graphql/introspection/directive_type.rb +23 -16
  11. data/lib/graphql/introspection/dynamic_fields.rb +11 -0
  12. data/lib/graphql/introspection/entry_points.rb +29 -0
  13. data/lib/graphql/introspection/enum_value_type.rb +16 -11
  14. data/lib/graphql/introspection/field_type.rb +21 -12
  15. data/lib/graphql/introspection/input_value_type.rb +26 -23
  16. data/lib/graphql/introspection/schema_field.rb +7 -2
  17. data/lib/graphql/introspection/schema_type.rb +36 -22
  18. data/lib/graphql/introspection/type_by_name_field.rb +10 -2
  19. data/lib/graphql/introspection/type_kind_enum.rb +10 -6
  20. data/lib/graphql/introspection/type_type.rb +85 -23
  21. data/lib/graphql/introspection/typename_field.rb +1 -0
  22. data/lib/graphql/language.rb +1 -0
  23. data/lib/graphql/language/document_from_schema_definition.rb +129 -37
  24. data/lib/graphql/language/generation.rb +3 -182
  25. data/lib/graphql/language/nodes.rb +12 -2
  26. data/lib/graphql/language/parser.rb +63 -55
  27. data/lib/graphql/language/parser.y +2 -1
  28. data/lib/graphql/language/printer.rb +351 -0
  29. data/lib/graphql/object_type.rb +1 -1
  30. data/lib/graphql/query.rb +1 -1
  31. data/lib/graphql/query/arguments.rb +24 -8
  32. data/lib/graphql/query/context.rb +3 -0
  33. data/lib/graphql/query/literal_input.rb +4 -1
  34. data/lib/graphql/railtie.rb +28 -6
  35. data/lib/graphql/schema.rb +42 -7
  36. data/lib/graphql/schema/enum.rb +1 -0
  37. data/lib/graphql/schema/field.rb +26 -5
  38. data/lib/graphql/schema/field/dynamic_resolve.rb +18 -9
  39. data/lib/graphql/schema/input_object.rb +2 -2
  40. data/lib/graphql/schema/introspection_system.rb +93 -0
  41. data/lib/graphql/schema/late_bound_type.rb +32 -0
  42. data/lib/graphql/schema/member.rb +21 -1
  43. data/lib/graphql/schema/member/build_type.rb +8 -6
  44. data/lib/graphql/schema/member/has_fields.rb +1 -1
  45. data/lib/graphql/schema/member/instrumentation.rb +3 -1
  46. data/lib/graphql/schema/member/list_type_proxy.rb +4 -0
  47. data/lib/graphql/schema/member/non_null_type_proxy.rb +4 -0
  48. data/lib/graphql/schema/object.rb +2 -1
  49. data/lib/graphql/schema/printer.rb +33 -266
  50. data/lib/graphql/schema/traversal.rb +74 -6
  51. data/lib/graphql/schema/validation.rb +3 -2
  52. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +6 -6
  53. data/lib/graphql/tracing/scout_tracing.rb +2 -2
  54. data/lib/graphql/upgrader/member.rb +463 -63
  55. data/lib/graphql/version.rb +1 -1
  56. data/spec/fixtures/upgrader/blame_range.original.rb +43 -0
  57. data/spec/fixtures/upgrader/blame_range.transformed.rb +31 -0
  58. data/spec/fixtures/upgrader/subscribable.original.rb +51 -0
  59. data/spec/fixtures/upgrader/subscribable.transformed.rb +46 -0
  60. data/spec/fixtures/upgrader/type_x.original.rb +35 -0
  61. data/spec/fixtures/upgrader/type_x.transformed.rb +35 -0
  62. data/spec/graphql/language/document_from_schema_definition_spec.rb +729 -296
  63. data/spec/graphql/language/generation_spec.rb +21 -186
  64. data/spec/graphql/language/nodes_spec.rb +21 -0
  65. data/spec/graphql/language/printer_spec.rb +203 -0
  66. data/spec/graphql/query/arguments_spec.rb +14 -4
  67. data/spec/graphql/query/context_spec.rb +17 -0
  68. data/spec/graphql/schema/build_from_definition_spec.rb +13 -4
  69. data/spec/graphql/schema/field_spec.rb +14 -0
  70. data/spec/graphql/schema/introspection_system_spec.rb +39 -0
  71. data/spec/graphql/schema/object_spec.rb +12 -1
  72. data/spec/graphql/schema/printer_spec.rb +14 -14
  73. data/spec/graphql/tracing/platform_tracing_spec.rb +2 -2
  74. data/spec/graphql/upgrader/member_spec.rb +274 -62
  75. data/spec/support/jazz.rb +75 -3
  76. metadata +38 -9
  77. data/lib/graphql/introspection/arguments_field.rb +0 -7
  78. data/lib/graphql/introspection/enum_values_field.rb +0 -18
  79. data/lib/graphql/introspection/fields_field.rb +0 -13
  80. data/lib/graphql/introspection/input_fields_field.rb +0 -12
  81. data/lib/graphql/introspection/interfaces_field.rb +0 -11
  82. data/lib/graphql/introspection/of_type_field.rb +0 -6
  83. data/lib/graphql/introspection/possible_types_field.rb +0 -11
@@ -25,15 +25,76 @@ module GraphQL
25
25
  Schema::BUILT_IN_INSTRUMENTERS +
26
26
  schema.instrumenters[:field_after_built_ins]
27
27
 
28
+ # These fields have types specified by _name_,
29
+ # So we need to inspect the schema and find those types,
30
+ # then update their references.
31
+ @late_bound_fields = []
28
32
  @type_map = {}
29
33
  @instrumented_field_map = Hash.new { |h, k| h[k] = {} }
30
34
  @type_reference_map = Hash.new { |h, k| h[k] = [] }
31
35
  @union_memberships = Hash.new { |h, k| h[k] = [] }
32
36
  visit(schema, schema, nil)
37
+ resolve_late_bound_fields
33
38
  end
34
39
 
35
40
  private
36
41
 
42
+ # A brute-force appraoch to late binding.
43
+ # Just keep trying the whole list, hoping that they
44
+ # eventually all resolve.
45
+ # This could be replaced with proper dependency tracking.
46
+ def resolve_late_bound_fields
47
+ # This is a bit tricky, with the writes going to internal state.
48
+ prev_late_bound_fields = @late_bound_fields
49
+ # Things might get added here during `visit...`
50
+ # or they might be added manually if we can't find them by hand
51
+ @late_bound_fields = []
52
+ prev_late_bound_fields.each do |(owner_type, field_defn, dynamic_field)|
53
+ if @type_map.key?(field_defn.type.unwrap.name)
54
+ late_bound_return_type = field_defn.type
55
+ resolved_type = @type_map.fetch(late_bound_return_type.unwrap.name)
56
+ wrapped_resolved_type = rewrap_resolved_type(late_bound_return_type, resolved_type)
57
+ # Update the field definition in place? :thinking_face:
58
+ field_defn.type = wrapped_resolved_type
59
+ visit_field_on_type(@schema, owner_type, field_defn, dynamic_field: dynamic_field)
60
+ else
61
+ @late_bound_fields << [owner_type, field_defn, dynamic_field]
62
+ end
63
+ end
64
+
65
+ if @late_bound_fields.any?
66
+ # If we visited each field and failed to resolve _any_,
67
+ # then we're stuck.
68
+ if @late_bound_fields == prev_late_bound_fields
69
+ type_names = prev_late_bound_fields.map { |f| f[1] }.map(&:type).map(&:unwrap).map(&:name).uniq
70
+ raise <<-ERR
71
+ Some late-bound types couldn't be resolved:
72
+
73
+ - #{type_names}
74
+ - Found __* types: #{@type_map.keys.select { |k| k.start_with?("__") }}
75
+ ERR
76
+ else
77
+ resolve_late_bound_fields
78
+ end
79
+ end
80
+ end
81
+
82
+ # The late-bound type may be wrapped with list or non-null types.
83
+ # Apply the same wrapping to the resolve type and
84
+ # return the maybe-wrapped type
85
+ def rewrap_resolved_type(late_bound_type, resolved_inner_type)
86
+ case late_bound_type
87
+ when GraphQL::NonNullType
88
+ rewrap_resolved_type(late_bound_type.of_type, resolved_inner_type).to_non_null_type
89
+ when GraphQL::ListType
90
+ rewrap_resolved_type(late_bound_type.of_type, resolved_inner_type).to_list_type
91
+ when GraphQL::Schema::LateBoundType
92
+ resolved_inner_type
93
+ else
94
+ raise "Unexpected late_bound_type: #{late_bound_type.inspect} (#{late_bound_type.class})"
95
+ end
96
+ end
97
+
37
98
  def visit(schema, member, context_description)
38
99
  case member
39
100
  when GraphQL::Schema
@@ -41,11 +102,14 @@ module GraphQL
41
102
  # Find the starting points, then visit them
42
103
  visit_roots = [member.query, member.mutation, member.subscription]
43
104
  if @introspection
44
- visit_roots << GraphQL::Introspection::SchemaType
105
+ introspection_types = schema.introspection_system.object_types
106
+ visit_roots.concat(introspection_types)
45
107
  if member.query
46
- # Visit this so that arguments class is preconstructed
47
- # Skip validation since it begins with __
48
- visit_field_on_type(schema, member.query, GraphQL::Introspection::TypeByNameField, dynamic_field: true)
108
+ member.introspection_system.entry_points.each do |introspection_field|
109
+ # Visit this so that arguments class is preconstructed
110
+ # Skip validation since it begins with "__"
111
+ visit_field_on_type(schema, member.query, introspection_field, dynamic_field: true)
112
+ end
49
113
  end
50
114
  end
51
115
  visit_roots.concat(member.orphan_types)
@@ -91,7 +155,7 @@ module GraphQL
91
155
  end
92
156
  elsif !prev_type.equal?(type_defn)
93
157
  # If the previous entry in the map isn't the same object we just found, raise.
94
- raise("Duplicate type definition found for name '#{type_defn.name}'")
158
+ raise("Duplicate type definition found for name '#{type_defn.name}' (#{prev_type.metadata[:object_class]}, #{type_defn.metadata[:object_class]}})")
95
159
  end
96
160
  when Class
97
161
  if member.respond_to?(:graphql_definition)
@@ -113,6 +177,11 @@ module GraphQL
113
177
  end
114
178
 
115
179
  def visit_field_on_type(schema, type_defn, field_defn, dynamic_field: false)
180
+ base_return_type = field_defn.type.unwrap
181
+ if base_return_type.is_a?(GraphQL::Schema::LateBoundType)
182
+ @late_bound_fields << [type_defn, field_defn, dynamic_field]
183
+ return
184
+ end
116
185
  if dynamic_field
117
186
  # Don't apply instrumentation to dynamic fields since they're shared constants
118
187
  instrumented_field_defn = field_defn
@@ -124,7 +193,6 @@ module GraphQL
124
193
  end
125
194
  @type_reference_map[instrumented_field_defn.type.unwrap.name] << instrumented_field_defn
126
195
  visit(schema, instrumented_field_defn.type, "Field #{type_defn.name}.#{instrumented_field_defn.name}'s return type")
127
-
128
196
  instrumented_field_defn.arguments.each do |name, arg|
129
197
  @type_reference_map[arg.type.unwrap.to_s] << arg
130
198
  visit(schema, arg.type, "Argument #{name} on #{type_defn.name}.#{instrumented_field_defn.name}")
@@ -24,7 +24,8 @@ module GraphQL
24
24
  # @param allowed_classes [Class] Classes which the return value may be an instance of
25
25
  # @return [Proc] A proc which will validate the input by calling `property_name` and asserting it is an instance of one of `allowed_classes`
26
26
  def self.assert_property(property_name, *allowed_classes)
27
- allowed_classes_message = allowed_classes.map(&:name).join(" or ")
27
+ # Hide LateBoundType from user-facing errors
28
+ allowed_classes_message = allowed_classes.map(&:name).reject {|n| n.include?("LateBoundType") }.join(" or ")
28
29
  ->(obj) {
29
30
  property_value = obj.public_send(property_name)
30
31
  is_valid_value = allowed_classes.any? { |allowed_class| property_value.is_a?(allowed_class) }
@@ -240,7 +241,7 @@ module GraphQL
240
241
  Rules::RESERVED_NAME,
241
242
  Rules::DESCRIPTION_IS_STRING_OR_NIL,
242
243
  Rules.assert_property(:deprecation_reason, String, NilClass),
243
- Rules.assert_property(:type, GraphQL::BaseType),
244
+ Rules.assert_property(:type, GraphQL::BaseType, GraphQL::Schema::LateBoundType),
244
245
  Rules.assert_property(:property, Symbol, NilClass),
245
246
  Rules::ARGUMENTS_ARE_STRING_TO_ARGUMENT,
246
247
  Rules::ARGUMENTS_ARE_VALID,
@@ -16,14 +16,14 @@ module GraphQL
16
16
  private
17
17
 
18
18
  def validate_field(context, ast_field, parent_type, parent)
19
- if parent_type.kind.union? && ast_field.name != '__typename'
20
- context.errors << message("Selections can't be made directly on unions (see selections on #{parent_type.name})", parent, context: context)
21
- return GraphQL::Language::Visitor::SKIP
22
- end
23
-
24
19
  field = context.warden.get_field(parent_type, ast_field.name)
20
+
25
21
  if field.nil?
26
- context.errors << message("Field '#{ast_field.name}' doesn't exist on type '#{parent_type.name}'", ast_field, context: context)
22
+ if parent_type.kind.union?
23
+ context.errors << message("Selections can't be made directly on unions (see selections on #{parent_type.name})", parent, context: context)
24
+ else
25
+ context.errors << message("Field '#{ast_field.name}' doesn't exist on type '#{parent_type.name}'", ast_field, context: context)
26
+ end
27
27
  return GraphQL::Language::Visitor::SKIP
28
28
  end
29
29
  end
@@ -16,9 +16,9 @@ module GraphQL
16
16
  "execute_query_lazy" => "execute.graphql",
17
17
  }
18
18
 
19
- def initialize
19
+ def initialize(options = {})
20
20
  self.class.include ScoutApm::Tracer
21
- super
21
+ super(options)
22
22
  end
23
23
 
24
24
  def platform_trace(platform_key, key, data)
@@ -1,111 +1,511 @@
1
1
  # frozen_string_literal: true
2
+ begin
3
+ require 'parser/current'
4
+ rescue LoadError
5
+ raise LoadError, "GraphQL::Upgrader requires the 'parser' gem, please install it and/or add it to your Gemfile"
6
+ end
2
7
 
3
8
  module GraphQL
4
9
  module Upgrader
5
- class Member
6
- def initialize(member)
7
- @member = member
10
+ GRAPHQL_TYPES = '(Object|InputObject|Interface|Enum|Scalar|Union)'
11
+
12
+ class Transform
13
+ # @param input_text [String] Untransformed GraphQL-Ruby code
14
+ # @param rewrite_options [Hash] Used during rewrite
15
+ # @return [String] The input text, with a transformation applied if necessary
16
+ def apply(input_text)
17
+ raise NotImplementedError, "Return transformed text here"
8
18
  end
9
19
 
10
- def upgrade
11
- transformable = member.dup
12
- transformable = transform_to_class transformable
13
- transformable = transform_or_remove_name transformable
14
- transformable = simplify_field_definition_for_easier_processing transformable
15
- transformable = move_the_type_from_the_block_to_the_field transformable
16
- transformable = rename_property_to_method transformable
20
+ # Recursively transform a `.define`-DSL-based type expression into a class-ready expression, for example:
21
+ #
22
+ # - `types[X]` -> `[X]`
23
+ # - `Int` -> `Integer`
24
+ # - `X!` -> `X`
25
+ #
26
+ # Notice that `!` is removed entirely, because it doesn't exist in the class API.
27
+ #
28
+ # @param type_expr [String] A `.define`-ready expression of a return type or input type
29
+ # @return [String] A class-ready expression of the same type`
30
+ def normalize_type_expression(type_expr, preserve_bang: false)
31
+ case type_expr
32
+ when /\A!/
33
+ # Handle the bang, normalize the inside
34
+ "#{preserve_bang ? "!" : ""}#{normalize_type_expression(type_expr[1..-1], preserve_bang: preserve_bang)}"
35
+ when /\Atypes\[.*\]\Z/
36
+ # Unwrap the brackets, normalize, then re-wrap
37
+ "[#{normalize_type_expression(type_expr[6..-2], preserve_bang: preserve_bang)}]"
38
+ when /\Atypes\./
39
+ # Remove the prefix
40
+ normalize_type_expression(type_expr[6..-1], preserve_bang: preserve_bang)
41
+ when /\A->/
42
+ # Remove the proc wrapper, don't re-apply it
43
+ # because stabby is not supported in class-based definition
44
+ # (and shouldn't ever be necessary)
45
+ unwrapped = type_expr
46
+ .sub(/\A->\s?\{\s*/, "")
47
+ .sub(/\s*\}/, "")
48
+ normalize_type_expression(unwrapped, preserve_bang: preserve_bang)
49
+ when "Int"
50
+ "Integer"
51
+ else
52
+ type_expr
53
+ end
54
+ end
55
+ end
56
+
57
+ # Turns `{X} = GraphQL::{Y}Type.define do` into `class {X} < Types::Base{Y}`.
58
+ class TypeDefineToClassTransform < Transform
59
+ # @param base_class_pattern [String] Replacement pattern for the base class name. Use this if your base classes have nonstandard names.
60
+ def initialize(base_class_pattern: "Types::Base\\2")
61
+ @find_pattern = /([a-zA-Z_0-9:]*) = GraphQL::#{GRAPHQL_TYPES}Type\.define do/
62
+ @replace_pattern = "class \\1 < #{base_class_pattern}"
63
+ end
64
+
65
+ def apply(input_text)
66
+ input_text.sub(@find_pattern, @replace_pattern)
67
+ end
68
+ end
69
+
70
+ # Remove `name "Something"` if it is redundant with the class name.
71
+ # Or, if it is not redundant, move it to `graphql_name "Something"`.
72
+ class NameTransform < Transform
73
+ def apply(transformable)
74
+ if (matches = transformable.match(/class (?<type_name>[a-zA-Z_0-9:]*) </))
75
+ type_name = matches[:type_name]
76
+ # Get the name without any prefixes or suffixes
77
+ type_name_without_the_type_part = type_name.split('::').last.gsub(/Type$/, '')
78
+ # Find an overridden name value
79
+ if matches = transformable.match(/ name ('|")(?<overridden_name>.*)('|")/)
80
+ name = matches[:overridden_name]
81
+ if type_name_without_the_type_part != name
82
+ # If the overridden name is still required, use `graphql_name` for it
83
+ transformable = transformable.sub(/ name (.*)/, ' graphql_name \1')
84
+ else
85
+ # Otherwise, remove it altogether
86
+ transformable = transformable.sub(/\s+name ('|").*('|")/, '')
87
+ end
88
+ end
89
+ end
90
+
91
+ transformable
92
+ end
93
+ end
94
+
95
+ # Remove newlines -- normalize the text for processing
96
+ class RemoveNewlinesTransform
97
+ def apply(input_text)
98
+ input_text.gsub(/(?<field>(?:field|connection|argument).*?,)\n(\s*)(?<next_line>(:?"|field)(.*))/) do
99
+ field = $~[:field].chomp
100
+ next_line = $~[:next_line]
101
+
102
+ "#{field} #{next_line}"
103
+ end
104
+ end
105
+ end
106
+
107
+ # Move `type X` to be the second positional argument to `field ...`
108
+ class PositionalTypeArgTransform < Transform
109
+ def apply(input_text)
110
+ input_text.gsub(
111
+ /(?<field>(?:field|connection|argument) :(?:[a-zA-Z_0-9]*)) do(?<block_contents>.*?)[ ]*type (?<return_type>.*?)\n/m
112
+ ) do
113
+ field = $~[:field]
114
+ block_contents = $~[:block_contents]
115
+ return_type = normalize_type_expression($~[:return_type], preserve_bang: true)
116
+
117
+ "#{field}, #{return_type} do#{block_contents}"
118
+ end
119
+ end
120
+ end
121
+
122
+ # Find a configuration in the block and move it to a kwarg,
123
+ # for example
124
+ # ```
125
+ # do
126
+ # property :thing
127
+ # end
128
+ # ```
129
+ # becomes:
130
+ # ```
131
+ # property: thing
132
+ # ```
133
+ class ConfigurationToKwargTransform < Transform
134
+ def initialize(kwarg:)
135
+ @kwarg = kwarg
136
+ end
137
+
138
+ def apply(input_text)
139
+ input_text.gsub(
140
+ /(?<field>(?:field|connection|argument).*) do(?<block_contents>.*?)[ ]*#{@kwarg} (?<kwarg_value>.*?)\n/m
141
+ ) do
142
+ field = $~[:field]
143
+ block_contents = $~[:block_contents]
144
+ kwarg_value = $~[:kwarg_value]
145
+
146
+ "#{field}, #{@kwarg}: #{kwarg_value} do#{block_contents}"
147
+ end
148
+ end
149
+ end
150
+
151
+ # Transform `property:` kwarg to `method:` kwarg
152
+ class PropertyToMethodTransform < Transform
153
+ def apply(input_text)
154
+ input_text.gsub /property:/, 'method:'
155
+ end
156
+ end
157
+
158
+ # Take camelized field names and convert them to underscore case.
159
+ # (They'll be automatically camelized later.)
160
+ class UnderscoreizeFieldNameTransform < Transform
161
+ def apply(input_text)
162
+ input_text.sub /(?<field_type>input_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)/ do
163
+ field_type = $~[:field_type]
164
+ camelized_name = $~[:name]
165
+ underscored_name = underscorize(camelized_name)
166
+ "#{field_type} :#{underscored_name}"
167
+ end
168
+ end
169
+
170
+ def underscorize(str)
171
+ str
172
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder
173
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2') # someThing -> some_Thing
174
+ .downcase
175
+ end
176
+ end
177
+
178
+ class ResolveProcToMethodTransform < Transform
179
+ def apply(input_text)
180
+ if input_text =~ /resolve ->/
181
+ # - Find the proc literal
182
+ # - Get the three argument names (obj, arg, ctx)
183
+ # - Get the proc body
184
+ # - Find and replace:
185
+ # - The ctx argument becomes `@context`
186
+ # - The obj argument becomes `@object`
187
+ # - Args is trickier:
188
+ # - If it's not used, remove it
189
+ # - If it's used, abandon ship and make it `**args`
190
+ # - (It would be nice to correctly become Ruby kwargs, but that might be too hard)
191
+ # - Add a `# TODO` comment to the method source?
192
+ # - Rebuild the method:
193
+ # - use the field name as the method name
194
+ # - handle args as described above
195
+ # - put the modified proc body as the method body
196
+
197
+ input_text.match(/(?<field_type>input_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)/)
198
+ field_name = $~[:name]
199
+ field_ast = Parser::CurrentRuby.parse(input_text)
200
+ processor = ResolveProcProcessor.new
201
+ processor.process(field_ast)
202
+ proc_body = input_text[processor.proc_start..processor.proc_end]
203
+ obj_arg_name, args_arg_name, ctx_arg_name = processor.proc_arg_names
204
+ # This is not good, it will hit false positives
205
+ # Should use AST to make this substitution
206
+ proc_body.gsub!(/([^\w:]|^)#{obj_arg_name}([^\w]|$)/, '\1@object\2')
207
+ proc_body.gsub!(/([^\w:]|^)#{ctx_arg_name}([^\w]|$)/, '\1@context\2')
208
+
209
+ indent = " " * processor.resolve_indent
210
+ prev_body_indent = "#{indent} "
211
+ next_body_indent = indent
212
+ method_def_indent = indent[2..-1]
213
+ # Turn the proc body into a method body
214
+ lines = proc_body.split("\n").map do |line|
215
+ line = line.sub(prev_body_indent, "")
216
+ "#{next_body_indent}#{line}".rstrip
217
+ end
218
+ # Add `def... end`
219
+ method_def = if input_text.include?("argument ")
220
+ # This field has arguments
221
+ "def #{field_name}(**#{args_arg_name})"
222
+ else
223
+ # No field arguments, so, no method arguments
224
+ "def #{field_name}"
225
+ end
226
+ lines.unshift("\n#{method_def_indent}#{method_def}")
227
+ lines << "#{method_def_indent}end\n"
228
+ method_body = lines.join("\n")
229
+ # Replace the resolve proc with the method
230
+ input_text[processor.resolve_start..processor.resolve_end] = ""
231
+ # The replacement above might have left some preceeding whitespace,
232
+ # so remove it by deleting all whitespace chars before `resolve`:
233
+ preceeding_whitespace = processor.resolve_start - 1
234
+ while input_text[preceeding_whitespace] == " " && preceeding_whitespace > 0
235
+ input_text[preceeding_whitespace] = ""
236
+ preceeding_whitespace -= 1
237
+ end
238
+ input_text += method_body
239
+ input_text
240
+ else
241
+ # No resolve proc
242
+ input_text
243
+ end
244
+ end
245
+
246
+
247
+ class ResolveProcProcessor < Parser::AST::Processor
248
+ attr_reader :proc_start, :proc_end, :proc_arg_names, :resolve_start, :resolve_end, :resolve_indent
249
+ def initialize
250
+ @proc_arg_names = nil
251
+ @resolve_start = nil
252
+ @resolve_end = nil
253
+ @resolve_indent = nil
254
+ @proc_start = nil
255
+ @proc_end = nil
256
+ end
257
+
258
+ def on_send(node)
259
+ receiver, method_name, _args = *node
260
+ if method_name == :resolve && receiver.nil?
261
+ source_exp = node.loc.expression
262
+ @resolve_start = source_exp.begin.begin_pos
263
+ @resolve_end = source_exp.end.end_pos
264
+ @resolve_indent = source_exp.column
265
+ end
266
+ super(node)
267
+ end
268
+
269
+ def on_block(node)
270
+ send_node, args_node, body_node = node.children
271
+ _receiver, method_name, _send_args_node = *send_node
272
+ if method_name == :lambda
273
+ source_exp = body_node.loc.expression
274
+ @proc_arg_names = args_node.children.map { |arg_node| arg_node.children[0].to_s }
275
+ @proc_start = source_exp.begin.begin_pos
276
+ @proc_end = source_exp.end.end_pos
277
+ end
278
+ super(node)
279
+ end
280
+ end
281
+ end
17
282
 
18
- transformable.scan(/(?:field|connection|argument) .*$/).each do |field|
19
- field_regex = /(?<field_type>field|connection|argument) :(?<name>[a-zA-Z_0-9]*)?, (?<return_type>.*?)(?<thing>,|$|\})(?<remainder>.*)/
283
+ # Transform `interfaces [A, B, C]` to `implements A\nimplements B\nimplements C\n`
284
+ class InterfacesToImplementsTransform < Transform
285
+ def apply(input_text)
286
+ input_text.gsub(
287
+ /(?<indent>\s*)(?:interfaces) \[\s*(?<interfaces>(?:[a-zA-Z_0-9:\.,\s]+))\]/m
288
+ ) do
289
+ indent = $~[:indent]
290
+ interfaces = $~[:interfaces].split(',').map(&:strip).reject(&:empty?)
291
+ interfaces.map do |interface|
292
+ "#{indent}implements #{interface}"
293
+ end.join
294
+ end
295
+ end
296
+ end
20
297
 
21
- if (matches = field_regex.match(field))
298
+ class UpdateMethodSignatureTransform < Transform
299
+ def apply(input_text)
300
+ input_text.scan(/(?:input_field|field|connection|argument) .*$/).each do |field|
301
+ matches = /(?<field_type>input_field|field|connection|argument) :(?<name>[a-zA-Z_0-9_]*)?, (?<return_type>[^,]*)(?<remainder>.*)/.match(field)
302
+ if matches
22
303
  name = matches[:name]
23
304
  return_type = matches[:return_type]
24
305
  remainder = matches[:remainder]
25
- thing = matches[:thing]
26
306
  field_type = matches[:field_type]
27
307
 
28
308
  # This is a small bug in the regex. Ideally the `do` part would only be in the remainder.
29
309
  with_block = remainder.gsub!(/\ do$/, '') || return_type.gsub!(/\ do$/, '')
30
310
 
31
- nullable = !!(return_type.gsub! '!', '')
32
- return_type.gsub! 'types.', ''
33
- return_type.gsub! 'types[', '['
311
+ remainder.gsub! /,$/, ''
312
+ remainder.gsub! /^,/, ''
313
+ remainder.chomp!
314
+
315
+ has_bang = !(return_type.gsub! '!', '')
316
+ return_type = normalize_type_expression(return_type)
317
+ return_type = return_type.gsub ',', ''
318
+
319
+ input_text.sub!(field) do
320
+ is_argument = ['argument', 'input_field'].include?(field_type)
321
+ f = "#{is_argument ? 'argument' : 'field'} :#{name}, #{return_type}"
322
+
323
+ unless remainder.empty?
324
+ f += ',' + remainder
325
+ end
326
+
327
+ if is_argument
328
+ if has_bang
329
+ f += ', required: false'
330
+ else
331
+ f += ', required: true'
332
+ end
333
+ else
334
+ if has_bang
335
+ f += ', null: true'
336
+ else
337
+ f += ', null: false'
338
+ end
339
+ end
34
340
 
35
- nullable_as_keyword = ", null: #{!nullable.to_s}"
36
- connection_as_keyword = field_type == 'connection' ? ', connection: true' : ''
37
- field_type = field_type == 'argument' ? 'argument' : 'field'
341
+ if field_type == 'connection'
342
+ f += ', connection: true'
343
+ end
38
344
 
39
- transformable.sub!(field) do
40
- "#{field_type} :#{name}, #{return_type}#{thing}#{remainder}#{nullable_as_keyword}#{connection_as_keyword}#{with_block ? ' do' : ''}"
345
+ if with_block
346
+ f += ' do'
347
+ end
348
+
349
+ f
41
350
  end
42
351
  end
43
352
  end
44
353
 
45
- transformable
354
+ input_text
46
355
  end
356
+ end
47
357
 
48
- def upgradeable?
49
- return false if member.include? '< GraphQL::Schema::'
50
- return false if member.include? '< BaseObject'
51
- return false if member.include? '< BaseInterface'
52
- return false if member.include? '< BaseEnum'
358
+ class RemoveEmptyBlocksTransform < Transform
359
+ def apply(input_text)
360
+ input_text.gsub(/\s*do\s*end/m, "")
361
+ end
362
+ end
53
363
 
54
- true
364
+ # Remove redundant newlines, which may have trailing spaces
365
+ # Remove double newline after `do`
366
+ class RemoveExcessWhitespaceTransform < Transform
367
+ def apply(input_text)
368
+ input_text
369
+ .gsub(/\n{3,}/m, "\n")
370
+ .gsub(/do\n\n/m, "do\n")
55
371
  end
372
+ end
56
373
 
57
- private
374
+ class Member
375
+ def initialize(member, type_transforms: DEFAULT_TYPE_TRANSFORMS, field_transforms: DEFAULT_FIELD_TRANSFORMS, clean_up_transforms: DEFAULT_CLEAN_UP_TRANSFORMS)
376
+ @member = member
377
+ @type_transforms = type_transforms
378
+ @field_transforms = field_transforms
379
+ @clean_up_transforms = clean_up_transforms
380
+ end
58
381
 
59
- def move_the_type_from_the_block_to_the_field(transformable)
60
- transformable.gsub(
61
- /(?<field>(?:field|connection|argument) :(?:[a-zA-Z_0-9]*)) do(?<block_contents>.*?)[ ]*type (?<return_type>.*?)\n/m
62
- ) do
63
- field = $~[:field]
64
- block_contents = $~[:block_contents]
65
- return_type = $~[:return_type]
382
+ DEFAULT_TYPE_TRANSFORMS = [
383
+ TypeDefineToClassTransform,
384
+ NameTransform,
385
+ InterfacesToImplementsTransform,
386
+ ]
66
387
 
67
- "#{field}, #{return_type} do#{block_contents}"
388
+ DEFAULT_FIELD_TRANSFORMS = [
389
+ RemoveNewlinesTransform,
390
+ PositionalTypeArgTransform,
391
+ ConfigurationToKwargTransform.new(kwarg: "property"),
392
+ ConfigurationToKwargTransform.new(kwarg: "description"),
393
+ PropertyToMethodTransform,
394
+ UnderscoreizeFieldNameTransform,
395
+ ResolveProcToMethodTransform,
396
+ UpdateMethodSignatureTransform,
397
+ ]
398
+
399
+ DEFAULT_CLEAN_UP_TRANSFORMS = [
400
+ RemoveExcessWhitespaceTransform,
401
+ RemoveEmptyBlocksTransform,
402
+ ]
403
+
404
+ def upgrade
405
+ # Transforms on type defn code:
406
+ type_source = apply_transforms(@member.dup, @type_transforms)
407
+ # Transforms on each field:
408
+ field_sources = find_fields(type_source)
409
+ field_sources.each do |field_source|
410
+ transformed_field_source = apply_transforms(field_source.dup, @field_transforms)
411
+ # Replace the original source code with the transformed source code:
412
+ type_source = type_source.gsub(field_source, transformed_field_source)
68
413
  end
414
+ # Clean-up:
415
+ type_source = apply_transforms(type_source, @clean_up_transforms)
416
+ # Return the transformed source:
417
+ type_source
69
418
  end
70
419
 
71
- def simplify_field_definition_for_easier_processing(transformable)
72
- transformable.gsub(/(?<field>(?:field|connection|argument).*?),\n(\s*)(?<next_line>(:?"|field)(.*))/) do
73
- field = $~[:field].chomp
74
- next_line = $~[:next_line]
420
+ def upgradeable?
421
+ return false if @member.include? '< GraphQL::Schema::'
422
+ return false if @member =~ /< Types::Base#{GRAPHQL_TYPES}/
75
423
 
76
- "#{field}, #{next_line}"
424
+ true
425
+ end
426
+
427
+ private
428
+
429
+ def apply_transforms(source_code, transforms, idx: 0)
430
+ next_transform = transforms[idx]
431
+ case next_transform
432
+ when nil
433
+ # We got to the end of the list
434
+ source_code
435
+ when Class
436
+ # Apply a class
437
+ next_source_code = next_transform.new.apply(source_code)
438
+ apply_transforms(next_source_code, transforms, idx: idx + 1)
439
+ else
440
+ # Apply an already-initialized object which responds to `apply`
441
+ next_source_code = next_transform.apply(source_code)
442
+ apply_transforms(next_source_code, transforms, idx: idx + 1)
77
443
  end
78
444
  end
79
445
 
80
- def transform_to_class(transformable)
81
- transformable.sub(
82
- /([a-zA-Z_0-9:]*) = GraphQL::(Object|Interface|Enum|Union)Type\.define do/, 'class \1 < Base\2'
83
- )
446
+ # Parse the type, find calls to `field` and `connection`
447
+ # Return strings containing those calls
448
+ def find_fields(type_source)
449
+ type_ast = Parser::CurrentRuby.parse(type_source)
450
+ finder = FieldFinder.new
451
+ finder.process(type_ast)
452
+ field_sources = []
453
+ # For each of the locations we found, extract the text for that definition.
454
+ # The text will be transformed independently,
455
+ # then the transformed text will replace the original text.
456
+ finder.locations.each do |name, (starting_idx, ending_idx)|
457
+ field_source = type_source[starting_idx..ending_idx]
458
+ field_sources << field_source
459
+ end
460
+ # Here's a crazy thing: the transformation is pure,
461
+ # so definitions like `argument :id, types.ID` can be transformed once
462
+ # then replaced everywhere. So:
463
+ # - make a unique array here
464
+ # - use `gsub` after performing the transformation.
465
+ field_sources.uniq!
466
+ field_sources
84
467
  end
85
468
 
86
- def transform_or_remove_name(transformable)
87
- if (matches = transformable.match(/class (?<type_name>[a-zA-Z_0-9]*) < Base(Object|Interface|Enum|Union)/))
88
- type_name = matches[:type_name]
89
- type_name_without_the_type_part = type_name.gsub(/Type$/, '')
469
+ class FieldFinder < Parser::AST::Processor
470
+ # These methods are definition DSLs which may accept a block,
471
+ # each of these definitions is passed for transformation in its own right
472
+ DEFINITION_METHODS = [:field, :connection, :input_field, :argument]
473
+ attr_reader :locations
90
474
 
91
- if matches = transformable.match(/name ('|")(?<type_name>.*)('|")/)
92
- name = matches[:type_name]
93
- if type_name_without_the_type_part != name
94
- transformable = transformable.sub(/name (.*)/, 'graphql_name \1')
95
- else
96
- transformable = transformable.sub(/\s*name ('|").*('|")/, '')
475
+ def initialize
476
+ # Pairs of `{ name => [start, end] }`,
477
+ # since we know fields are unique by name.
478
+ @locations = {}
479
+ end
480
+
481
+ # @param send_node [node] The node which might be a `field` call, etc
482
+ # @param source_node [node] The node whose source defines the bounds of the definition (eg, the surrounding block)
483
+ def add_location(send_node:,source_node:)
484
+ receiver_node, method_name, *arg_nodes = *send_node
485
+ # Implicit self and one of the recognized methods
486
+ if receiver_node.nil? && DEFINITION_METHODS.include?(method_name)
487
+ name = arg_nodes[0]
488
+ # This field may have already been added because
489
+ # we find `(block ...)` nodes _before_ we find `(send ...)` nodes.
490
+ if @locations[name].nil?
491
+ starting_idx = source_node.loc.expression.begin.begin_pos
492
+ ending_idx = source_node.loc.expression.end.end_pos
493
+ @locations[name] = [starting_idx, ending_idx]
97
494
  end
98
495
  end
99
496
  end
100
497
 
101
- transformable
102
- end
498
+ def on_block(node)
499
+ send_node, _args_node, _body_node = *node
500
+ add_location(send_node: send_node, source_node: node)
501
+ super(node)
502
+ end
103
503
 
104
- def rename_property_to_method(transformable)
105
- transformable.gsub /property:/, 'method:'
504
+ def on_send(node)
505
+ add_location(send_node: node, source_node: node)
506
+ super(node)
507
+ end
106
508
  end
107
-
108
- attr_reader :member
109
509
  end
110
510
  end
111
511
  end