-
Notifications
You must be signed in to change notification settings - Fork 5
/
base.rb
502 lines (418 loc) · 13.9 KB
/
base.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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
require 'repp'
require 'whenever'
require 'parse-cron'
require "mobb/version"
module Mobb
class Matcher
def initialize(pattern, options = {}) @pattern, @options = pattern, options; end
def regexp?; pattern.is_a?(Regexp); end
def inspect; "pattern: #{@pattern}, options #{@options}"; end
def cron?; pattern.is_a?(CronParser); end
def tick(time = Time.now) @options[:last_tick] = time; end
def last_tick; @options[:last_tick] end
def match?(context)
case context
when String
string_matcher(context)
when Time
cron_matcher(context)
when Array
context.all? { |c| match?(c) }
else
false
end
end
class Matched
attr_reader :pattern, :matched
def initialize(pattern, matched) @pattern, @matched = pattern, matched; end
end
def pattern; @pattern; end
def string_matcher(string)
case pattern
when Regexp
if res = pattern.match(string)
Matched.new(pattern, res.captures)
else
false
end
when String
@options[:laziness] ? string.include?(pattern) : string == pattern
else
false
end
end
def cron_matcher(time)
last = last_tick
tick(time) if !last || time > last
return false if time < last
pattern.next(time) == pattern.next(last) ? false : true
end
end
class Base
def call(env)
dup.call!(env)
end
def call!(env)
@env = env
invoke { dispatch! }
[@body, @attachments]
end
def dispatch!
# TODO: encode input messages
invoke do
filter! :before
handle_event
end
ensure
begin
filter! :after
rescue ::Exception => boom
# TODO: invoke { handle_exception!(boom) }
end
end
def invoke
res = catch(:halt) { yield }
return if res.nil?
res = [res] if String === res
if Array === res && String === res.first
tmp = res.dup
@body = tmp.shift
@attachments = tmp.pop
else
@attachments = res
end
nil
end
def filter!(type, base = settings)
filter! type, base.superclass if base.superclass.respond_to?(:filters)
base.filters[type].each { |signature|
# TODO: Refactor compile! and process_event to change conditions in a hash (e,g, { source_cond: [], dest_cond: [] })
pattern = signature.first
source_conditions = signature[2]
wrapper = signature[1]
process_event(pattern, source_conditions, wrapper)
}
end
def handle_event(base = settings, passed_block = nil)
if responds = base.events[@env.event_type]
responds.each do |pattern, block, source_conditions, dest_conditions|
process_event(pattern, source_conditions) do |*args|
event_eval do
res = block[*args]
dest_conditions.inject(res) { |acc, c| c.bind(self).call(acc) }
end
end
end
end
# TODO: Define respond missing if receive reply message
nil
end
def process_event(pattern, conditions, block = nil, values = [])
res = pattern.match?(@env.body)
catch(:pass) do
conditions.each { |c| throw :pass unless c.bind(self).call }
case res
when ::Mobb::Matcher::Matched
block ? block[self, *(res.matched)] : yield(self, *(res.matched))
when TrueClass
block ? block[self] : yield(self)
else
nil
end
end
end
def event_eval; throw :halt, yield; end
def settings
self.class.settings
end
class << self
CALLERS_TO_IGNORE = [
/\/mobb(\/(base|main|show_exceptions))?\.rb$/, # all sinatra code
/^\(.*\)$/, # generated code
/rubygems\/(custom|core_ext\/kernel)_require\.rb$/, # rubygems require hacks
/active_support/, # active_support require hacks
/bundler(\/runtime)?\.rb/, # bundler require hacks
/<internal:/, # internal in ruby >= 1.9.2
/src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
]
attr_reader :events, :filters
def reset!
@events = {}
@filters = { before: [], after: [] }
@source_conditions = []
@dest_conditions = []
@extensions = []
end
def extensions
if superclass.respond_to?(:extensions)
(@extensions + superclass.extensions).uniq
else
@extensions
end
end
def settings
self
end
def before(pattern = /.*/, **options, &block)
add_filter(:before, pattern, options, &block)
end
def after(pattern = /.*/, **options, &block)
add_filter(:after, pattern, options, &block)
end
def add_filter(type, pattern = /.*/, **options, &block)
filters[type] << compile!(type, pattern, options, &block)
end
def receive(pattern, options = {}, &block) event(:message, pattern, options, &block); end
alias :on :receive
def cron(pattern, options = {}, &block) event(:ticker, pattern, options, &block); end
alias :every :cron
def event(type, pattern, options, &block)
signature = compile!(type, pattern, options, &block)
(@events[type] ||= []) << signature
invoke_hook(:event_added, type, pattern, block)
signature
end
def invoke_hook(name, *args)
extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
end
def compile!(type, pattern, options, &block)
at = options.delete(:at)
options.each_pair { |option, args| send(option, *args) }
matcher = case type
when :message
compile(pattern, options)
when :ticker
compile_cron(pattern, at)
else
compile(pattern, options)
end
unbound_method = generate_method("#{type}", &block)
source_conditions, @source_conditions = @source_conditions, []
dest_conditions, @dest_conditions = @dest_conditions, []
wrapper = block.arity != 0 ?
proc { |instance, args| unbound_method.bind(instance).call(*args) } :
proc { |instance, args| unbound_method.bind(instance).call }
[ matcher, wrapper, source_conditions, dest_conditions ]
end
def compile(pattern, options) Matcher.new(pattern, options); end
def compile_cron(time, at)
if String === time
Matcher.new(CronParser.new(time))
else
Matcher.new(CronParser.new(Whenever::Output::Cron.new(time, nil, at).time_in_cron_syntax))
end
end
def generate_method(name, &block)
define_method(name, &block)
method = instance_method(name)
remove_method(name)
method
end
def helpers(*extensions, &block)
class_eval(&block) if block_given?
include(*extensions) if extensions.any?
end
def register(*extensions, &block)
extensions << Module.new(&block) if block_given?
@extensions += extensions
extensions.each do |extension|
extend extension
extension.registered(self) if extension.respond_to?(:registered)
end
end
def development?; environment == :development; end
def production?; environment == :production; end
def test?; environment == :test; end
def set(option, value = (not_set = true), ignore_setter = false, &block)
raise ArgumentError if block && !not_set
value, not_set = block, false if block
if not_set
raise ArgumentError unless option.respond_to?(:each)
option.each { |k,v| set(k,v) }
return self
end
setter_name = "#{option}="
if respond_to?(setter_name) && ! ignore_setter
return __send__(setter_name, value)
end
setter = proc { |val| set(option, val, true) }
getter = proc { value }
case value
when Proc
getter = value
when Symbol, Integer, FalseClass, TrueClass, NilClass
getter = value.inspect
when Hash
setter = proc do |val|
val = value.merge(val) if Hash === val
set(option, val, true)
end
end
define_singleton(setter_name, setter)
define_singleton(option, getter)
define_singleton("#{option}?", "!!#{option}") unless method_defined?("#{option}?")
self
end
def condition(name = "#{caller.first[/`.*'/]} condition", &block)
@source_conditions << generate_method(name, &block)
end
alias :source_condition :condition
def dest_condition(name = "#{caller.first[/`.*'/]} condition", &block)
@dest_conditions << generate_method(name) do |res|
if String === res
res = [res, {}]
end
block.call(res)
res
end
end
def ignore_bot(cond)
condition do
@env.bot? != cond
end
end
def reply_to_me(cond)
condition do
@env.reply_to.include?(settings.name) == cond
end
end
def dest_to(channel)
dest_condition do |res|
res.last[:dest_channel] = channel
end
end
def enable(*options) options.each { |option| set(option, true) }; end
def disable(*options) options.each { |option| set(option, false) }; end
def clear(*options) options.each { |option| set(option, nil) }; end
def run!(options = {}, &block)
return if running?
set options
handler = detect_repp_handler
handler_name = handler.name.gsub(/.*::/, '')
service_settings = settings.respond_to?(:service_settings) ? settings.service_settings : {}
begin
start_service(handler, service_settings, handler_name, &block)
rescue => e
$stderr.puts e.message
$stderr.puts e.backtrace
ensure
quit!
end
end
def quit!
return unless running?
running_service.respond_to?(:stop!) ? running_service.stop! : running_service.stop
$stderr.puts "== Great sound Mobb, thank you so much"
clear :running_service, :handler_name
end
def running?
running_service?
end
private
def start_service(handler, service_settings, handler_name)
handler.run(self, service_settings) do |service|
$stderr.puts "== Mobb (v#{Mobb::VERSION}) is in da house with #{handler_name}. Make some noise!"
setup_traps
set running_service: service
set handler_name: handler_name
yield service if block_given?
end
end
def setup_traps
if traps?
at_exit { quit! }
[:INT, :TERM].each do |signal|
old_handler = Signal.trap(signal) do
quit!
old_handler.respond_to?(:call) ? old_handler.call : exit
end
end
disable :traps
end
end
def detect_repp_handler
services = Array(service)
services.each do |service_name|
begin
return Repp::Handler.get(service_name.to_s)
rescue LoadError, NameError
end
end
fail "Service handler (#{services.join(',')}) not found"
end
def define_singleton(name, content = Proc.new)
singleton_class.class_eval do
undef_method(name) if method_defined?(name)
String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
end
end
def caller_files
cleaned_caller(1).flatten
end
def cleaned_caller(keep = 3)
caller(1).
map! { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
end
def inherited(subclass)
subclass.reset!
subclass.set :app_file, caller_files.first unless subclass.app_file?
super
end
end
reset!
set :name, 'mobb'
set :environment, (ENV['APP_ENV'] || ENV['REPP_ENV'] || :development).to_sym
disable :run, :quiet
clear :running_service, :handler_name
enable :traps
set :service, %w[shell]
clear :app_file
end
class Application < Base
set :logging, Proc.new { !test? }
set :run, Proc.new { !test? }
clear :app_file
def self.register(*extensions, &block)
added_methods = extensions.flat_map(&:public_instance_methods)
Delegator.delegate(*added_methods)
super(*extensions, &block)
end
end
module Delegator
def self.delegate(*methods)
methods.each do |method_name|
define_method(method_name) do |*args, &block|
return super(*args, &block) if respond_to? method_name
Delegator.target.send(method_name, *args, &block)
end
private method_name
end
end
delegate :receive, :on, :every, :cron,
:set, :enable, :disable, :clear, :before, :after,
:helpers, :register
class << self
attr_accessor :target
end
self.target = Application
end
# Create a new Mobb application; the block is evaluated in the class scope.
def self.new(base = Base, &block)
base = Class.new(base)
base.class_eval(&block) if block_given?
base
end
# Extend the top-level DSL with the modules provided.
def self.register(*extensions, &block)
Delegator.target.register(*extensions, &block)
end
# Include the helper modules provided in Mobb's request context.
def self.helpers(*extensions, &block)
Delegator.target.helpers(*extensions, &block)
end
# Use the middleware for classic applications.
def self.use(*args, &block)
Delegator.target.use(*args, &block)
end
end