Skip to content

Commit ffc68de

Browse files
committed
Start on graphql-migrate-execution
1 parent 269ce55 commit ffc68de

6 files changed

Lines changed: 337 additions & 0 deletions

File tree

bin/graphql-migrate-execution

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
require "graphql/migrate_execution"
4+
require "optparse"
5+
6+
doctor_options = {}
7+
parser = OptionParser.new
8+
parser.on("-p", "--preview", "Preview migration strategies")
9+
parser.parse!(into: doctor_options)
10+
filename = ARGV.shift || raise(ArgumentError, "graphql-execution-doctor require a filename or path as a first argument, please pass one.")
11+
12+
doctor = GraphQL::MigrateExecution.new(filename, **doctor_options)
13+
doctor.run

lib/graphql/migrate_execution.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
require "prism"
3+
require "pp"
4+
require "graphql/migrate_execution/field_definition"
5+
require "graphql/migrate_execution/resolver_method"
6+
require "graphql/migrate_execution/type_definition"
7+
require "graphql/migrate_execution/visitor"
8+
9+
module GraphQL
10+
class MigrateExecution
11+
NOT_CONFIGURED = Object.new
12+
def initialize(filepath)
13+
@filepath = filepath
14+
end
15+
16+
def run
17+
source = File.read(@filepath)
18+
parse_result = Prism.parse(source)
19+
visitor = Visitor.new(source)
20+
visitor.visit(parse_result.value)
21+
22+
field_definitions_by_strategy = Hash.new { |h, k| h[k] = [] }
23+
24+
total_defns = 0
25+
visitor.type_definitions.each do |name, type_defn|
26+
type_defn.field_definitions.each do |f_name, f_defn|
27+
total_defns += 1
28+
field_definitions_by_strategy[f_defn.migration_strategy] << f_defn
29+
end
30+
end
31+
32+
puts "Found #{total_defns} field definitions:"
33+
field_definitions_by_strategy.each do |strategy, definitions|
34+
puts "\n#{strategy} (#{definitions.size}):"
35+
max_path = definitions.map { |f| f.path.size }.max + 2
36+
definitions.each do |field_defn|
37+
name = field_defn.path.ljust(max_path)
38+
puts " - #{name} (#{field_defn.resolve_mode.inspect} -> #{field_defn.resolve_mode_key.inspect}) @ #{@filepath}:#{field_defn.source_line}"
39+
end
40+
end
41+
end
42+
end
43+
end
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
module GraphQL
3+
class MigrateExecution
4+
class FieldDefinition
5+
def initialize(type_definition, name, node)
6+
@type_definition = type_definition
7+
@name = name.to_sym
8+
@node = node
9+
10+
@resolve_mode = NOT_CONFIGURED
11+
@hash_key = nil
12+
@resolver = nil
13+
@type_instance_method = nil
14+
@object_direct_method = nil
15+
16+
@resolver_method = nil
17+
@unknown_options = []
18+
end
19+
20+
def migration_strategy
21+
case resolve_mode
22+
when NOT_CONFIGURED, nil, :implicit_resolve
23+
:MANUAL
24+
when :hash_key, :object_direct_method
25+
:NONE_REQUIRED
26+
when :type_instance_method
27+
@resolver_method.migration_strategy
28+
when :resolver
29+
:TODO
30+
else
31+
raise "No migration strategy for resolve_mode #{@resolve_mode.inspect}"
32+
end
33+
end
34+
35+
attr_reader :name, :node, :resolver_method, :unknown_options, :type_definition
36+
37+
attr_writer :resolve_mode
38+
39+
attr_accessor :hash_key, :object_direct_method, :type_instance_method, :resolver
40+
41+
def path
42+
@path ||= "#{type_definition.name}.#{@name}"
43+
end
44+
45+
def source_line
46+
@node.location.start_line
47+
end
48+
49+
def resolve_mode
50+
if NOT_CONFIGURED.equal?(@resolve_mode) || (@resolve_mode == :type_instance_method && @resolver_method.nil?)
51+
method_name = @type_instance_method || @name
52+
if (rm = @type_definition.resolver_methods[method_name])
53+
@resolver_method = rm
54+
@resolve_mode = :type_instance_method
55+
@type_instance_method ||= @name
56+
else
57+
@resolve_mode = :implicit_resolve
58+
end
59+
end
60+
@resolve_mode
61+
end
62+
63+
def implicit_resolve
64+
@name
65+
end
66+
67+
def resolve_mode_key
68+
resolve_mode && public_send(resolve_mode)
69+
end
70+
end
71+
end
72+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
module GraphQL
3+
class MigrateExecution
4+
5+
class ResolverMethod
6+
def initialize(name, node)
7+
@name = name
8+
@node = node
9+
@parameter_names = if node.parameters
10+
node.parameters.keywords.map(&:name)
11+
else
12+
[]
13+
end
14+
@self_sends = Set.new
15+
@calls_object = false
16+
@calls_context = false
17+
end
18+
19+
attr_reader :name, :node, :parameter_names, :self_sends
20+
21+
attr_accessor :calls_object, :calls_context # TODO dataloader, others?
22+
23+
def migration_strategy
24+
calls_to_self = self_sends.to_a
25+
if @calls_context
26+
calls_to_self.delete(:context)
27+
end
28+
if @calls_object
29+
calls_to_self.delete(:object)
30+
end
31+
# These will be migrated to context:
32+
calls_to_self.delete(:dataloader)
33+
calls_to_self.delete(:dataload_association)
34+
calls_to_self.delete(:dataload_record)
35+
36+
# Global-ish methods:
37+
calls_to_self.delete(:raise)
38+
39+
# Locals:
40+
calls_to_self -= @parameter_names
41+
42+
if calls_to_self.empty?
43+
if calls_object
44+
:RESOLVE_EACH
45+
else
46+
:STATIC_RESOLVE
47+
end
48+
else
49+
:TODO
50+
end
51+
end
52+
private
53+
54+
def calls_to_self
55+
56+
end
57+
end
58+
end
59+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
module GraphQL
3+
class MigrateExecution
4+
class TypeDefinition
5+
def initialize(name)
6+
@name = name
7+
@field_definitions = {}
8+
@resolver_methods = {}
9+
end
10+
11+
attr_reader :resolver_methods, :name, :field_definitions
12+
13+
def field_definition(name, node)
14+
@field_definitions[name] = FieldDefinition.new(self, name, node)
15+
end
16+
17+
def resolver_method(name, node)
18+
@resolver_methods[name] = ResolverMethod.new(name, node)
19+
end
20+
end
21+
end
22+
end
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# frozen_string_literal: true
2+
module GraphQL
3+
class MigrateExecution
4+
class Visitor < Prism::Visitor
5+
def initialize(source)
6+
@source = source
7+
@new_source = source.dup
8+
@type_definitions = Hash.new { |h, k| h[k] = TypeDefinition.new(k) }
9+
@type_definition_stack = []
10+
@current_field_definition = nil
11+
@current_resolver_method = nil
12+
end
13+
14+
attr_reader :new_source, :type_definitions
15+
16+
def visit_class_node(node)
17+
if node.superclass
18+
td = @type_definitions[node.name]
19+
@type_definition_stack << td
20+
end
21+
super
22+
ensure
23+
if td
24+
@type_definition_stack.pop
25+
end
26+
end
27+
28+
def visit_module_node(node)
29+
td = @type_definitions[node.name]
30+
@type_definition_stack << td
31+
super
32+
ensure
33+
@type_definition_stack.pop
34+
end
35+
36+
def visit_keyword_hash_node(node)
37+
if @current_field_definition
38+
node.elements.each do |assoc|
39+
if assoc.key.is_a?(Prism::SymbolNode)
40+
case assoc.key.unescaped
41+
when "hash_key"
42+
@current_field_definition.resolve_mode = :hash_key
43+
@current_field_definition.hash_key = get_keyword_value(assoc.value)
44+
when "resolver"
45+
@current_field_definition.resolve_mode = :resolver
46+
@current_field_definition.resolver = get_keyword_value(assoc.value)
47+
when "method"
48+
@current_field_definition.resolve_mode = :object_direct_method
49+
@current_field_definition.object_direct_method = get_keyword_value(assoc.value)
50+
when "resolver_method"
51+
@current_field_definition.resolve_mode = :type_instance_method
52+
@current_field_definition.type_instance_method = get_keyword_value(assoc.value)
53+
else
54+
# fallback_value, dig, connection, extensions, extras
55+
@current_field_definition.unknown_options << assoc.key.unescaped
56+
end
57+
end
58+
end
59+
end
60+
super
61+
end
62+
63+
def visit_call_node(node)
64+
if node.receiver.nil? && node.name == :field
65+
begin
66+
first_arg = node.arguments.arguments.first # rubocop:disable Development/ContextIsPassedCop
67+
if first_arg.is_a?(Prism::SymbolNode)
68+
field_name = first_arg.unescaped
69+
td = @type_definition_stack.last
70+
@current_field_definition = td.field_definition(field_name, node)
71+
else
72+
puts "Skipping unrecognized field definition: #{node.inspect}"
73+
end
74+
end
75+
elsif @current_resolver_method
76+
if node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
77+
@current_resolver_method.self_sends.add(node.name)
78+
if node.name == :object
79+
@current_resolver_method.calls_object = true
80+
elsif node.name == :context
81+
@current_resolver_method.calls_object = true
82+
end
83+
end
84+
end
85+
super
86+
ensure
87+
if td
88+
@current_field_definition = nil
89+
end
90+
end
91+
92+
def visit_def_node(node)
93+
if node.receiver.nil?
94+
td = @type_definition_stack.last
95+
@current_resolver_method = td.resolver_method(node.name, node)
96+
end
97+
super
98+
ensure
99+
@current_resolver_method = nil
100+
end
101+
102+
103+
private
104+
105+
def get_keyword_value(value_node)
106+
case value_node
107+
when Prism::SymbolNode
108+
value_node.unescaped.to_sym
109+
when Prism::StringNode
110+
value_node.unescaped
111+
when Prism::IntegerNode, Prism::FloatNode
112+
value_node.value
113+
when Prism::TrueNode
114+
true
115+
when Prism::FalseNode
116+
false
117+
when Prism::ConstantReadNode
118+
value_node.name
119+
when Prism::CallNode
120+
:DYNAMIC_CALL_NODE
121+
else
122+
# nil, constants, `self` ...?
123+
raise ArgumentError, "GraphQL-MigrateExecution can't parse this keyword argument yet, but it could. Please open an issue on GraphQL-Ruby with this error message (node class: #{value_node.class})\n\n#{value_node.inspect}"
124+
end
125+
end
126+
end
127+
end
128+
end

0 commit comments

Comments
 (0)