-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
visitor.rb
90 lines (79 loc) · 2.83 KB
/
visitor.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# frozen_string_literal: true
module GraphQL
module Language
# Depth-first traversal through the tree, calling hooks at each stop.
#
# @example Create a visitor, add hooks, then search a document
# total_field_count = 0
# visitor = GraphQL::Language::Visitor.new(document)
# # Whenever you find a field, increment the field count:
# visitor[GraphQL::Language::Nodes::Field] << ->(node) { total_field_count += 1 }
# # When we finish, print the field count:
# visitor[GraphQL::Language::Nodes::Document].leave << ->(node) { p total_field_count }
# visitor.visit
# # => 6
#
class Visitor
# If any hook returns this value, the {Visitor} stops visiting this
# node right away
SKIP = :_skip
def initialize(document)
@document = document
@visitors = {}
end
# Get a {NodeVisitor} for `node_class`
# @param node_class [Class] The node class that you want to listen to
# @return [NodeVisitor]
#
# @example Run a hook whenever you enter a new Field
# visitor[GraphQL::Language::Nodes::Field] << ->(node, parent) { p "Here's a field" }
def [](node_class)
@visitors[node_class] ||= NodeVisitor.new
end
# Visit `document` and all children, applying hooks as you go
# @return [void]
def visit
visit_node(@document, nil)
end
private
def visit_node(node, parent)
begin_hooks_ok = begin_visit(node, parent)
if begin_hooks_ok
node.children.each { |child| visit_node(child, node) }
end
end_visit(node, parent)
end
def begin_visit(node, parent)
node_visitor = self[node.class]
self.class.apply_hooks(node_visitor.enter, node, parent)
end
# Should global `leave` visitors come first or last?
def end_visit(node, parent)
node_visitor = self[node.class]
self.class.apply_hooks(node_visitor.leave, node, parent)
end
# If one of the visitors returns SKIP, stop visiting this node
def self.apply_hooks(hooks, node, parent)
hooks.reduce(true) { |memo, proc| memo && (proc.call(node, parent) != SKIP) }
end
# Collect `enter` and `leave` hooks for classes in {GraphQL::Language::Nodes}
#
# Access {NodeVisitor}s via {GraphQL::Language::Visitor#[]}
class NodeVisitor
# @return [Array<Proc>] Hooks to call when entering a node of this type
attr_reader :enter
# @return [Array<Proc>] Hooks to call when leaving a node of this type
attr_reader :leave
def initialize
@enter = []
@leave = []
end
# Shorthand to add a hook to the {#enter} array
# @param hook [Proc] A hook to add
def <<(hook)
enter << hook
end
end
end
end
end