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.
- checksums.yaml +4 -4
- data/lib/graphql.rb +1 -1
- data/lib/graphql/deprecated_dsl.rb +2 -0
- data/lib/graphql/enum_type.rb +1 -1
- data/lib/graphql/field.rb +10 -1
- data/lib/graphql/input_object_type.rb +3 -1
- data/lib/graphql/introspection.rb +3 -10
- data/lib/graphql/introspection/base_object.rb +15 -0
- data/lib/graphql/introspection/directive_location_enum.rb +11 -7
- data/lib/graphql/introspection/directive_type.rb +23 -16
- data/lib/graphql/introspection/dynamic_fields.rb +11 -0
- data/lib/graphql/introspection/entry_points.rb +29 -0
- data/lib/graphql/introspection/enum_value_type.rb +16 -11
- data/lib/graphql/introspection/field_type.rb +21 -12
- data/lib/graphql/introspection/input_value_type.rb +26 -23
- data/lib/graphql/introspection/schema_field.rb +7 -2
- data/lib/graphql/introspection/schema_type.rb +36 -22
- data/lib/graphql/introspection/type_by_name_field.rb +10 -2
- data/lib/graphql/introspection/type_kind_enum.rb +10 -6
- data/lib/graphql/introspection/type_type.rb +85 -23
- data/lib/graphql/introspection/typename_field.rb +1 -0
- data/lib/graphql/language.rb +1 -0
- data/lib/graphql/language/document_from_schema_definition.rb +129 -37
- data/lib/graphql/language/generation.rb +3 -182
- data/lib/graphql/language/nodes.rb +12 -2
- data/lib/graphql/language/parser.rb +63 -55
- data/lib/graphql/language/parser.y +2 -1
- data/lib/graphql/language/printer.rb +351 -0
- data/lib/graphql/object_type.rb +1 -1
- data/lib/graphql/query.rb +1 -1
- data/lib/graphql/query/arguments.rb +24 -8
- data/lib/graphql/query/context.rb +3 -0
- data/lib/graphql/query/literal_input.rb +4 -1
- data/lib/graphql/railtie.rb +28 -6
- data/lib/graphql/schema.rb +42 -7
- data/lib/graphql/schema/enum.rb +1 -0
- data/lib/graphql/schema/field.rb +26 -5
- data/lib/graphql/schema/field/dynamic_resolve.rb +18 -9
- data/lib/graphql/schema/input_object.rb +2 -2
- data/lib/graphql/schema/introspection_system.rb +93 -0
- data/lib/graphql/schema/late_bound_type.rb +32 -0
- data/lib/graphql/schema/member.rb +21 -1
- data/lib/graphql/schema/member/build_type.rb +8 -6
- data/lib/graphql/schema/member/has_fields.rb +1 -1
- data/lib/graphql/schema/member/instrumentation.rb +3 -1
- data/lib/graphql/schema/member/list_type_proxy.rb +4 -0
- data/lib/graphql/schema/member/non_null_type_proxy.rb +4 -0
- data/lib/graphql/schema/object.rb +2 -1
- data/lib/graphql/schema/printer.rb +33 -266
- data/lib/graphql/schema/traversal.rb +74 -6
- data/lib/graphql/schema/validation.rb +3 -2
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +6 -6
- data/lib/graphql/tracing/scout_tracing.rb +2 -2
- data/lib/graphql/upgrader/member.rb +463 -63
- data/lib/graphql/version.rb +1 -1
- data/spec/fixtures/upgrader/blame_range.original.rb +43 -0
- data/spec/fixtures/upgrader/blame_range.transformed.rb +31 -0
- data/spec/fixtures/upgrader/subscribable.original.rb +51 -0
- data/spec/fixtures/upgrader/subscribable.transformed.rb +46 -0
- data/spec/fixtures/upgrader/type_x.original.rb +35 -0
- data/spec/fixtures/upgrader/type_x.transformed.rb +35 -0
- data/spec/graphql/language/document_from_schema_definition_spec.rb +729 -296
- data/spec/graphql/language/generation_spec.rb +21 -186
- data/spec/graphql/language/nodes_spec.rb +21 -0
- data/spec/graphql/language/printer_spec.rb +203 -0
- data/spec/graphql/query/arguments_spec.rb +14 -4
- data/spec/graphql/query/context_spec.rb +17 -0
- data/spec/graphql/schema/build_from_definition_spec.rb +13 -4
- data/spec/graphql/schema/field_spec.rb +14 -0
- data/spec/graphql/schema/introspection_system_spec.rb +39 -0
- data/spec/graphql/schema/object_spec.rb +12 -1
- data/spec/graphql/schema/printer_spec.rb +14 -14
- data/spec/graphql/tracing/platform_tracing_spec.rb +2 -2
- data/spec/graphql/upgrader/member_spec.rb +274 -62
- data/spec/support/jazz.rb +75 -3
- metadata +38 -9
- data/lib/graphql/introspection/arguments_field.rb +0 -7
- data/lib/graphql/introspection/enum_values_field.rb +0 -18
- data/lib/graphql/introspection/fields_field.rb +0 -13
- data/lib/graphql/introspection/input_fields_field.rb +0 -12
- data/lib/graphql/introspection/interfaces_field.rb +0 -11
- data/lib/graphql/introspection/of_type_field.rb +0 -6
- 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
|
-
|
105
|
+
introspection_types = schema.introspection_system.object_types
|
106
|
+
visit_roots.concat(introspection_types)
|
45
107
|
if member.query
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
341
|
+
if field_type == 'connection'
|
342
|
+
f += ', connection: true'
|
343
|
+
end
|
38
344
|
|
39
|
-
|
40
|
-
|
345
|
+
if with_block
|
346
|
+
f += ' do'
|
347
|
+
end
|
348
|
+
|
349
|
+
f
|
41
350
|
end
|
42
351
|
end
|
43
352
|
end
|
44
353
|
|
45
|
-
|
354
|
+
input_text
|
46
355
|
end
|
356
|
+
end
|
47
357
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
block_contents = $~[:block_contents]
|
65
|
-
return_type = $~[:return_type]
|
382
|
+
DEFAULT_TYPE_TRANSFORMS = [
|
383
|
+
TypeDefineToClassTransform,
|
384
|
+
NameTransform,
|
385
|
+
InterfacesToImplementsTransform,
|
386
|
+
]
|
66
387
|
|
67
|
-
|
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
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
102
|
-
|
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
|
-
|
105
|
-
|
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
|