graft 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4127a179ba1dd78b8575fac807051f3072a4fed22ec5f1521afe25626d3432a5
4
+ data.tar.gz: 4cfe831c87a9b0b2be7b2a37945825a6329d15fbcf5d037ea08c492855f26f68
5
+ SHA512:
6
+ metadata.gz: cdbab8db3916a2f8f62d2563a0c2da6a3a4391573ee77277f6f381f4daf1b8af3185341a3c94efd9d3fbc63e03047b5e2b2dddeb0ef859e3f3af1bc9e97d523a
7
+ data.tar.gz: 7becc0d2ae1f84fcb34057005b43ec6cfb40bbe2069e735e263c22c15cea5b383c828ceabb9e88c87b64cd8bdc4c5c7e55b7dda0cc136df2e6369cb1103dd9cc
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graft
4
+ class Callback
5
+ attr_reader :name
6
+
7
+ # NOTE: opts is not used in the current implementation
8
+ def initialize(name = nil, opts = {}, &block)
9
+ @name = name
10
+ @opts = opts
11
+ @block = block
12
+ @enabled = true
13
+ end
14
+
15
+ if RUBY_VERSION < "3.0"
16
+ def call(*args, &block)
17
+ return unless enabled?
18
+
19
+ @block.call(*args, &block)
20
+ end
21
+ else
22
+ def call(*args, **kwargs, &block)
23
+ return unless enabled?
24
+
25
+ @block.call(*args, **kwargs, &block)
26
+ end
27
+ end
28
+
29
+ def disable
30
+ @enabled = false
31
+ end
32
+
33
+ def enable
34
+ @enabled = true
35
+ end
36
+
37
+ def enabled?
38
+ @enabled
39
+ end
40
+ end
41
+ end
data/lib/graft/hook.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stack"
4
+ require_relative "callback"
5
+ require_relative "hook_point"
6
+
7
+ module Graft
8
+ class Hook
9
+ DEFAULT_STRATEGY = HookPoint::DEFAULT_STRATEGY
10
+
11
+ @hooks = {}
12
+
13
+ def self.[](hook_point, strategy = DEFAULT_STRATEGY)
14
+ @hooks[hook_point] ||= new(hook_point, nil, strategy)
15
+ end
16
+
17
+ def self.add(hook_point, strategy = DEFAULT_STRATEGY, &block)
18
+ self[hook_point, strategy].add(&block)
19
+ end
20
+
21
+ def self.ignore
22
+ Thread.current[:hook_entered] = true
23
+ yield
24
+ ensure
25
+ Thread.current[:hook_entered] = false
26
+ end
27
+
28
+ attr_reader :point, :stack
29
+
30
+ def initialize(hook_point, dependency_test = nil, strategy = DEFAULT_STRATEGY)
31
+ @disabled = false
32
+ @point = hook_point.is_a?(HookPoint) ? hook_point : HookPoint.new(hook_point, strategy)
33
+ @dependency_test = dependency_test || proc { point.exist? }
34
+ @stack = Stack.new
35
+ end
36
+
37
+ def dependency?
38
+ return true if @dependency_test.nil?
39
+
40
+ @dependency_test.call
41
+ end
42
+
43
+ def add(&block)
44
+ tap { instance_eval(&block) }
45
+ end
46
+
47
+ def callback_name(tag = nil)
48
+ point.to_s << (tag ? ":#{tag}" : "")
49
+ end
50
+
51
+ def append(tag = nil, opts = {}, &block)
52
+ @stack << Callback.new(callback_name(tag), opts, &block)
53
+ end
54
+
55
+ def unshift(tag = nil, opts = {}, &block)
56
+ @stack.unshift Callback.new(callback_name(tag), opts, &block)
57
+ end
58
+
59
+ def before(tag = nil, opts = {}, &block)
60
+ # TODO
61
+ end
62
+
63
+ def after(tag = nil, opts = {}, &block)
64
+ # TODO
65
+ end
66
+
67
+ def depends_on(&block)
68
+ @dependency_test = block
69
+ end
70
+
71
+ def enable
72
+ @disabled = false
73
+ end
74
+
75
+ def disable
76
+ @disabled = true
77
+ end
78
+
79
+ def disabled?
80
+ @disabled
81
+ end
82
+
83
+ def install
84
+ return unless point.exist?
85
+
86
+ point.install("hook", &Hook.wrapper(self))
87
+ end
88
+
89
+ def uninstall
90
+ return unless point.exist?
91
+
92
+ point.uninstall("hook")
93
+ end
94
+
95
+ class << self
96
+ if RUBY_VERSION < "3.0"
97
+ def wrapper(hook)
98
+ proc do |*args, &block|
99
+ env = {
100
+ self: self,
101
+ args: args,
102
+ block: block
103
+ }
104
+ supa = proc { |*args, &block| super(*args, &block) }
105
+ mid = proc { |_, env| {return: supa.call(*env[:args], &env[:block])} }
106
+ stack = hook.stack.dup
107
+ stack << mid
108
+
109
+ stack.call(env)
110
+ end
111
+ end
112
+ else
113
+ def wrapper(hook)
114
+ proc do |*args, **kwargs, &block|
115
+ env = {
116
+ receiver: self,
117
+ method: hook.point.method_name,
118
+ kind: hook.point.method_kind,
119
+ strategy: hook.point.instance_variable_get(:@strategy),
120
+ args: args,
121
+ kwargs: kwargs,
122
+ block: block
123
+ }
124
+
125
+ supa = proc { |*args, **kwargs, &block| super(*args, **kwargs, &block) }
126
+ mid = proc { |_, env| {return: supa.call(*env[:args], **env[:kwargs], &env[:block])} }
127
+ stack = hook.stack.dup
128
+ stack << mid
129
+
130
+ stack.call(env)[:return]
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,410 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graft
4
+ class HookPointError < StandardError; end
5
+
6
+ class HookModule < Module
7
+ def initialize(key)
8
+ super()
9
+
10
+ @key = key
11
+ end
12
+
13
+ attr_reader :key
14
+
15
+ def inspect
16
+ "#<#{self.class.name}: #{@key.inspect}>"
17
+ end
18
+ end
19
+
20
+ class HookPoint
21
+ DEFAULT_STRATEGY = Module.respond_to?(:prepend) ? :prepend : :chain
22
+
23
+ class << self
24
+ def parse(hook_point)
25
+ klass_name, separator, method_name = hook_point.split(/(\#|\.)/, 2)
26
+
27
+ raise ArgumentError, hook_point if klass_name.nil? || separator.nil? || method_name.nil?
28
+ raise ArgumentError, hook_point unless [".", "#"].include?(separator)
29
+
30
+ method_kind = (separator == ".") ? :klass_method : :instance_method
31
+
32
+ [klass_name.to_sym, method_kind, method_name.to_sym]
33
+ end
34
+
35
+ def const_exist?(name)
36
+ resolve_const(name) && true
37
+ rescue NameError, ArgumentError
38
+ false
39
+ end
40
+
41
+ def resolve_const(name)
42
+ raise ArgumentError, "const not found: #{name}" if name.nil? || name.empty?
43
+
44
+ name.to_s.split("::").inject(Object) { |a, e| a.const_get(e, false) }
45
+ end
46
+
47
+ def resolve_module(name)
48
+ const = resolve_const(name)
49
+
50
+ raise ArgumentError, "not a Module: #{name}" unless const.is_a?(Module)
51
+
52
+ const
53
+ end
54
+
55
+ def strategy_module(strategy)
56
+ case strategy
57
+ when :prepend then Prepend
58
+ when :chain then Chain
59
+ else
60
+ raise HookPointError, "unknown strategy: #{strategy.inspect}"
61
+ end
62
+ end
63
+ end
64
+
65
+ attr_reader :klass_name, :method_kind, :method_name
66
+
67
+ def initialize(hook_point, strategy = DEFAULT_STRATEGY)
68
+ @klass_name, @method_kind, @method_name = HookPoint.parse(hook_point)
69
+ @strategy = strategy
70
+
71
+ extend HookPoint.strategy_module(strategy)
72
+ end
73
+
74
+ def to_s
75
+ @to_s ||= "#{@klass_name}#{(@method_kind == :instance_method) ? "#" : "."}#{@method_name}"
76
+ end
77
+
78
+ def exist?
79
+ return false unless HookPoint.const_exist?(@klass_name)
80
+
81
+ if klass_method?
82
+ (
83
+ klass.singleton_class.public_instance_methods(false) +
84
+ klass.singleton_class.protected_instance_methods(false) +
85
+ klass.singleton_class.private_instance_methods(false)
86
+ ).include?(@method_name)
87
+ elsif instance_method?
88
+ (
89
+ klass.public_instance_methods(false) +
90
+ klass.protected_instance_methods(false) +
91
+ klass.private_instance_methods(false)
92
+ ).include?(@method_name)
93
+ else
94
+ raise HookPointError, "#{self} unknown hook point kind"
95
+ end
96
+ end
97
+
98
+ def klass
99
+ HookPoint.resolve_module(@klass_name)
100
+ end
101
+
102
+ def klass_method?
103
+ @method_kind == :klass_method
104
+ end
105
+
106
+ def instance_method?
107
+ @method_kind == :instance_method
108
+ end
109
+
110
+ def private_method?
111
+ if klass_method?
112
+ klass.private_methods.include?(@method_name)
113
+ elsif instance_method?
114
+ klass.private_instance_methods.include?(@method_name)
115
+ else
116
+ raise HookPointError, "#{self} unknown hook point kind"
117
+ end
118
+ end
119
+
120
+ def protected_method?
121
+ if klass_method?
122
+ klass.protected_methods.include?(@method_name)
123
+ elsif instance_method?
124
+ klass.protected_instance_methods.include?(@method_name)
125
+ else
126
+ raise HookPointError, "#{self} unknown hook point kind"
127
+ end
128
+ end
129
+
130
+ def installed?(key) # rubocop:disable Lint/UselessMethodDefinition
131
+ super
132
+ end
133
+
134
+ def install(key, &block)
135
+ return unless exist?
136
+
137
+ return if installed?(key)
138
+
139
+ super
140
+ end
141
+
142
+ def uninstall(key)
143
+ return unless exist?
144
+
145
+ return unless installed?(key)
146
+
147
+ super
148
+ end
149
+
150
+ def enable(key) # rubocop:disable Lint/UselessMethodDefinition
151
+ super
152
+ end
153
+
154
+ def disable(key) # rubocop:disable Lint/UselessMethodDefinition
155
+ super
156
+ end
157
+
158
+ def disabled?(key) # rubocop:disable Lint/UselessMethodDefinition
159
+ super
160
+ end
161
+
162
+ module Prepend
163
+ def installed?(key)
164
+ prepended?(key) && overridden?(key)
165
+ end
166
+
167
+ def install(key, &block)
168
+ prepend(key)
169
+ override(key, &block)
170
+ end
171
+
172
+ def uninstall(key)
173
+ unoverride(key) if overridden?(key)
174
+ end
175
+
176
+ def enable(key)
177
+ raise HookPointError, "enable(#{key.inspect}) with prepend strategy"
178
+ end
179
+
180
+ def disable(key)
181
+ unoverride(key)
182
+ end
183
+
184
+ def disabled?(key)
185
+ !overridden?(key)
186
+ end
187
+
188
+ private
189
+
190
+ def hook_module(key)
191
+ target = klass_method? ? klass.singleton_class : klass
192
+
193
+ mod = target.ancestors.each do |e|
194
+ break if e == target
195
+ break(e) if e.instance_of?(HookModule) && e.key == key
196
+ end
197
+
198
+ raise HookPointError, "Inconsistency detected: #{target} missing from its own ancestors" if mod.is_a?(Array)
199
+
200
+ [target, mod]
201
+ end
202
+
203
+ def prepend(key)
204
+ target, mod = hook_module(key)
205
+
206
+ mod ||= HookModule.new(key)
207
+
208
+ target.prepend(mod)
209
+ end
210
+
211
+ def prepended?(key)
212
+ _, mod = hook_module(key)
213
+
214
+ mod != nil
215
+ end
216
+
217
+ def overridden?(key)
218
+ _, mod = hook_module(key)
219
+
220
+ (
221
+ mod.instance_methods(false) +
222
+ mod.protected_instance_methods(false) +
223
+ mod.private_instance_methods(false)
224
+ ).include?(method_name)
225
+ end
226
+
227
+ def override(key, &block)
228
+ hook_point = self
229
+ method_name = @method_name
230
+
231
+ _, mod = hook_module(key)
232
+
233
+ mod.instance_eval do
234
+ if hook_point.private_method?
235
+ private
236
+ elsif hook_point.protected_method?
237
+ protected
238
+ else
239
+ public
240
+ end
241
+
242
+ define_method(:"#{method_name}", &block)
243
+ end
244
+ end
245
+
246
+ def unoverride(key)
247
+ method_name = @method_name
248
+
249
+ _, mod = hook_module(key)
250
+
251
+ mod.instance_eval { remove_method(method_name) }
252
+ end
253
+ end
254
+
255
+ module Chain
256
+ def installed?(key)
257
+ defined(key)
258
+ end
259
+
260
+ def install(key, &block)
261
+ define(key, &block)
262
+ chain(key)
263
+ end
264
+
265
+ def uninstall(key)
266
+ disable(key)
267
+ remove(key)
268
+ end
269
+
270
+ def enable(key)
271
+ chain(key)
272
+ end
273
+
274
+ def disable(key)
275
+ unchain(key)
276
+ end
277
+
278
+ def disabled?(key)
279
+ !chained?(key)
280
+ end
281
+
282
+ private
283
+
284
+ def defined(suffix)
285
+ if klass_method?
286
+ (
287
+ klass.methods +
288
+ klass.protected_methods +
289
+ klass.private_methods
290
+ ).include?(:"#{method_name}_with_#{suffix}")
291
+ elsif instance_method?
292
+ (
293
+ klass.instance_methods +
294
+ klass.protected_instance_methods +
295
+ klass.private_instance_methods
296
+ ).include?(:"#{method_name}_with_#{suffix}")
297
+ else
298
+ raise HookPointError, "#{self} unknown hook point kind"
299
+ end
300
+ end
301
+
302
+ def define(suffix, &block)
303
+ hook_point = self
304
+ method_name = @method_name
305
+
306
+ if klass_method?
307
+ klass.singleton_class.instance_eval do
308
+ if hook_point.private_method?
309
+ private
310
+ elsif hook_point.protected_method?
311
+ protected
312
+ else
313
+ public
314
+ end
315
+
316
+ define_method(:"#{method_name}_with_#{suffix}", &block)
317
+ end
318
+ elsif instance_method?
319
+ klass.class_eval do
320
+ if hook_point.private_method?
321
+ private
322
+ elsif hook_point.protected_method?
323
+ protected
324
+ else
325
+ public
326
+ end
327
+
328
+ define_method(:"#{method_name}_with_#{suffix}", &block)
329
+ end
330
+ else
331
+ raise HookPointError, "unknown hook point kind"
332
+ end
333
+ end
334
+
335
+ def remove(suffix)
336
+ method_name = @method_name
337
+
338
+ if klass_method?
339
+ klass.singleton_class.instance_eval do
340
+ remove_method(:"#{method_name}_with_#{suffix}")
341
+ end
342
+ elsif instance_method?
343
+ klass.class_eval do
344
+ remove_method(:"#{method_name}_with_#{suffix}")
345
+ end
346
+ else
347
+ raise HookPointError, "unknown hook point kind"
348
+ end
349
+ end
350
+
351
+ def chained?(suffix)
352
+ method_name = @method_name
353
+
354
+ if klass_method?
355
+ klass.singleton_class.instance_eval do
356
+ instance_method(:"#{method_name}").original_name == :"#{method_name}_with_#{suffix}"
357
+ end
358
+ elsif instance_method?
359
+ klass.class_eval do
360
+ instance_method(:"#{method_name}").original_name == :"#{method_name}_with_#{suffix}"
361
+ end
362
+ else
363
+ raise HookPointError, "unknown hook point kind"
364
+ end
365
+ end
366
+
367
+ def chain(suffix)
368
+ method_name = @method_name
369
+
370
+ if klass_method?
371
+ klass.singleton_class.instance_eval do
372
+ alias_method :"#{method_name}_without_#{suffix}", :"#{method_name}"
373
+ alias_method :"#{method_name}", :"#{method_name}_with_#{suffix}"
374
+ end
375
+ elsif instance_method?
376
+ klass.class_eval do
377
+ alias_method :"#{method_name}_without_#{suffix}", :"#{method_name}"
378
+ alias_method :"#{method_name}", :"#{method_name}_with_#{suffix}"
379
+ end
380
+ else
381
+ raise HookPointError, "unknown hook point kind"
382
+ end
383
+ end
384
+
385
+ def unchain(suffix)
386
+ method_name = @method_name
387
+
388
+ if klass_method?
389
+ klass.singleton_class.instance_eval do
390
+ alias_method :"#{method_name}", :"#{method_name}_without_#{suffix}"
391
+ end
392
+ elsif instance_method?
393
+ klass.class_eval do
394
+ alias_method :"#{method_name}", :"#{method_name}_without_#{suffix}"
395
+ end
396
+ end
397
+ end
398
+
399
+ if RUBY_VERSION < "3.0"
400
+ def apply(obj, suffix, *args, &block)
401
+ obj.send(:"#{method_name}_without_#{suffix}", *args, &block)
402
+ end
403
+ else
404
+ def apply(obj, suffix, *args, **kwargs, &block)
405
+ obj.send(:"#{method_name}_without_#{suffix}", *args, **kwargs, &block)
406
+ end
407
+ end
408
+ end
409
+ end
410
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Graft
4
+ class Stack < Array
5
+ def call(env = {})
6
+ head.call(tail, env)
7
+ end
8
+
9
+ def head
10
+ # TODO: raise EmptyStackError?
11
+
12
+ first
13
+ end
14
+
15
+ def tail
16
+ tail = self[1..size]
17
+
18
+ # TODO: raise EmptyStackError?
19
+ return Stack.new if tail.nil?
20
+
21
+ Stack.new(tail)
22
+ end
23
+ end
24
+ end
data/lib/graft.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "graft/hook"
4
+
5
+ module Graft; end