graft 0.2.1 → 0.3.0

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 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