-
Notifications
You must be signed in to change notification settings - Fork 1
/
01_using_as_refinement.rb
122 lines (95 loc) · 3.82 KB
/
01_using_as_refinement.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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Gemmy provides some patches to core Ruby
# There are two ways to include them: as refinements, or globally.
# As refinements, they will only exist within a certain scope.
# The keywords 'using' and 'refine' appeared in ruby 2.0.
# 'using' in particular has some limitations, but it's the only way I've found
# to restrict the scope of patches.
# Firsly, 'using' cannot be abstracted into any method.
# It must be called from the top-level scope in a module/class,
# and it cannot be invoked dynamically.
# Despite this, 'using' is not all that bad. It makes the patch methods
# available to both class and instance scopes without any further code.
# It accepts a variable for it's classname argument and can be invoked in a
# loop as well. With that in mind, this is the best I could do:
class Tester
Gemmy::Patches.class_refinements.each { |r| using r }
nothing.eql?(nil) # Object#nothing patch
def self.refined?
new.refined?
end
def refined?
nothing.nil? # does a bear shit in the woods?
end
end
# Secondly, the patched matcheds can't be used outside of their original method
# definitions. That is to say, they can't be used with define_method or
# referenced in a block passed as an argument. The one exception I found is
# 'eval', but a wrapper method must be explicitly written.
# All the following will raise errors because of the way 'using' works:
nothing.eql?(nil) rescue "error was expected"
Tester.class.send(:eval, "nothing.eql?(nil)") rescue "error was expected"
Tester.send(:eval, "nothing.eql?(nil)") rescue "error was expected"
Tester.define_singleton_method(:test_method) { nothing.eql?(nil) }
Tester.test_method rescue "error was expected"
# Passed blocks also can't access the refined methods:
class Tester
Gemmy::Patches.class_refinements.each { |r| using r }
def self.call_block(&blk)
blk.call
end
end
Tester.call_block { nothing.eql?(nil) } rescue "error was expected"
# Eval does work if it's not invoked with 'send':
class Tester
Gemmy::Patches.class_refinements.each { |r| using r }
def self.call_eval(string)
eval string
end
end
Tester.call_eval %{
nothing.eql?(nil)
}
# Other than the core object patches, some other methods can be loaded onto the
# top level scope. When using refinements, this requires a call to
# include or extend.
class Tester
include Gemmy::Components
# These are methods from Gemmy::Components::DynamicSteps
define_step(/(. )/) { |x| "you said #{x}" }
step "hello" # => you said hello
end
# The reason all this is mentioned is that it's not necessary when Gemmy's
# patches are applied globally. In that case, the components include/extend
# happens automatically.
# Patches can also be cherrypicked for use with refinements.
# In the application's internal structure, this required wrapping all
# patch methods in their own modules.
# There's a method "Gemmy::Patches.method_refinements" that will replace
# "Gemmy::Patches.class_refinements" in this case. It's argument is a
# hash with special syntax:
class Tester
Gemmy::Patches.method_refinements(
String: { InstanceMethods: [:Unindent] },
Array: { InstanceMethods: [:Recurse, :AnyNot] }
).each { |klass| using klass }
def self.refined?
new.refined?
end
def refined?
# showing a few methods being used
" hello\n world".unindent == "hello\nworld" # true
[nil, [nil]].recurse(&:compact).any_not? { |x| !!x } # false
# other patches aren't defined
defined? nothing # false
end
end
# There's also a shortcut to cherrypicking a few modules:
Gemmy.patch("symbol/i/call") ==\
Gemmy::Patches::SymbolPatch::InstanceMethods::Call
Gemmy.patch("array/c/wrap") == \
Gemmy::Patches::ArrayPatch::ClassMethods::Wrap
# You can do something like:
module Test
using Gemmy.patch("symbol/i/call")
[[]].map(&:push.(1)) == [[1]]
end