ru.Bee 1.11.1 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6c1764a41eaa86619270fbaf3077cb5fc55b8d9c6b4f4edac37c2ac7f584fb7
4
- data.tar.gz: e298000985901fea481a623fd1a08759b6be441251dba2efd78d0120f1ee16f1
3
+ metadata.gz: c12dfae68f0595266581855939b1d4b80468ad9e4298438a0125b709e96c3fa3
4
+ data.tar.gz: 6fb74be8efa80754b66f603bbe41b2ba5cc9cbc26eddfcb55ee24f7073c31a74
5
5
  SHA512:
6
- metadata.gz: d11f20305783059eef64fa35530e2c6861ff708db527e6ff628b8d735b833d3ec565ce7bbeb4faa9ac0fb98d7868007c8c8728804ecd07adf37a49fc7f665c20
7
- data.tar.gz: 91b9a63e9bc18c4be04d6e11b097607c01733921b6ba9e33134ea0ee07780fbdf4352083e76f901f741856048732f2a55b8998451a735260aa61da05df80644c
6
+ metadata.gz: 463291b6a407cc39fa1b26620416da495598aa56332091e37eb5c8545e0d1f1a35bbb23586b43958bd7c83d59c66663b382b7c54c7b45a5496803365ee1e1623
7
+ data.tar.gz: 9bc34459746bc4a060f1fdb697f332cc35246cc104cf1a211ec9533f92a301573f0270e98582d86c64bfe72585fb233170befbc7e5719cb7d88d76a465945d3f
@@ -0,0 +1,57 @@
1
+ class UsersController < Rubee::BaseController
2
+ attach_websocket! # Method required to turn controller to been able to handle websocket requests
3
+ using ChargedHash
4
+
5
+ # Endpoint to find or create user
6
+ def create
7
+ user = User.where(**params).last
8
+ user ||= User.create(**params)
9
+
10
+ response_with(object: user, type: :json)
11
+ rescue StandardError => e
12
+ response_with(object: { error: e.message }, type: :json)
13
+ end
14
+
15
+ def subscribe
16
+ channel = params[:channel]
17
+ sender_id = params[:options][:id]
18
+ io = params[:options][:io]
19
+
20
+ User.sub(channel, sender_id, io) do |channel, args|
21
+ websocket_connections.register(channel, args[:io])
22
+ end
23
+
24
+ response_with(object: { type: 'system', channel: params[:channel], status: :subscribed }, type: :websocket)
25
+ rescue StandardError => e
26
+ response_with(object: { type: 'system', error: e.message }, type: :websocket)
27
+ end
28
+
29
+ def unsubscribe
30
+ channel = params[:channel]
31
+ sender_id = params[:options][:id]
32
+ io = params[:options][:io]
33
+
34
+ User.unsub(channel, sender_id, io) do |channel, args|
35
+ websocket_connections.remove(channel, args[:io])
36
+ end
37
+
38
+ response_with(object: params.merge(type: 'system', status: :unsubscribed), type: :websocket)
39
+ rescue StandardError => e
40
+ response_with(object: { type: 'system', error: e.message }, type: :websocket)
41
+ end
42
+
43
+ def publish
44
+ args = {}
45
+ User.pub(params[:channel], message: params[:message]) do |channel|
46
+ user = User.find(params[:options][:id])
47
+ args[:message] = params[:message]
48
+ args[:sender] = params[:options][:id]
49
+ args[:sender_name] = user.email
50
+ websocket_connections.stream(channel, args)
51
+ end
52
+
53
+ response_with(object: { type: 'system', message: params[:message], status: :published }, type: :websocket)
54
+ rescue StandardError => e
55
+ response_with(object: { type: 'system', error: e.message }, type: :websocket)
56
+ end
57
+ end
@@ -1,4 +1,6 @@
1
1
  class WelcomeController < Rubee::BaseController
2
+ using ChargedHash
3
+
2
4
  def show
3
5
  response_with
4
6
  end
data/lib/config/routes.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  Rubee::Router.draw do |router|
2
2
  router.get('/', to: 'welcome#show') # override it for your app
3
+ router.get('/ws', to: 'users#websocket')
3
4
  end
@@ -0,0 +1,7 @@
1
+ def reload
2
+ app_files = Dir["./#{Rubee::APP_ROOT}/**/*.rb"]
3
+ app_files.each { |file| load(file) }
4
+ puts "\e[32mReloaded..\e[0m"
5
+ end
6
+
7
+
@@ -1,5 +1,6 @@
1
1
  module Rubee
2
2
  class Autoload
3
+ BLACKLIST = ['rubee.rb', 'test_helper.rb']
3
4
  class << self
4
5
  def call(black_list = [], **options)
5
6
  load_whitelisted(options[:white_list_dirs]) && return if options[:white_list_dirs]
@@ -12,7 +13,7 @@ module Rubee
12
13
  Dir.glob(File.join(Rubee::APP_ROOT, '**', '*.rb')).sort.each do |file|
13
14
  base_name = File.basename(file)
14
15
 
15
- unless base_name.end_with?('_test.rb') || (black_list + ['rubee.rb', 'test_helper.rb']).include?(base_name)
16
+ unless base_name.end_with?('_test.rb') || (black_list + BLACKLIST).include?(base_name)
16
17
  require_relative file
17
18
  end
18
19
  end
@@ -35,6 +36,14 @@ module Rubee
35
36
  Dir[File.join(Rubee::APP_ROOT, 'inits/**', '*.rb')].each do |file|
36
37
  require_relative file unless black_list.include?("#{file}.rb")
37
38
  end
39
+ # rubee pub sub
40
+ Dir[File.join(root_directory, 'rubee/pubsub/**', '*.rb')].each do |file|
41
+ require_relative file unless black_list.include?("#{file}.rb")
42
+ end
43
+ # rubee websocket
44
+ Dir[File.join(root_directory, 'rubee/websocket/**', '*.rb')].each do |file|
45
+ require_relative file unless black_list.include?("#{file}.rb")
46
+ end
38
47
  # rubee async
39
48
  Dir[File.join(root_directory, 'rubee/async/**', '*.rb')].each do |file|
40
49
  require_relative file unless black_list.include?("#{file}.rb")
@@ -14,14 +14,6 @@ module Rubee
14
14
  Rubee::Configuration.setup(env = :test) do |config|
15
15
  config.database_url = { url: 'sqlite://lib/tests/test.db', env: }
16
16
  end
17
- # Rubee::Autoload.call
18
- # Rubee::SequelObject.reconnect!
19
- end
20
-
21
- def reload
22
- app_files = Dir["./#{Rubee::APP_ROOT}/**/*.rb"]
23
- app_files.each { |file| load(file) }
24
- color_puts('Reloaded ..', color: :green)
25
17
  end
26
18
 
27
19
  begin
@@ -29,7 +29,7 @@ module Rubee
29
29
  FileUtils.mkdir_p(target_dir)
30
30
  # Define blacklist
31
31
  blacklist_files = %w[rubee.rb print_colors.rb version.rb config.ru test_helper.rb Gemfile.lock test.yml test.db
32
- development.db production.db]
32
+ development.db production.db users_controller.rb users_controller.rb]
33
33
  blacklist_dirs = %w[rubee tests .git .github .idea node_modules db inits]
34
34
  # Copy files, excluding blacklisted ones
35
35
  copy_project_files(source_dir, target_dir, blacklist_files, blacklist_dirs)
@@ -108,6 +108,11 @@ module Rubee
108
108
  gem 'json'
109
109
  gem 'jwt'
110
110
 
111
+ # Websocket is required to use integrated websocket feature
112
+ gem 'websocket'
113
+ # Redis is required for pubsub and websocket
114
+ gem 'redis'
115
+
111
116
  group :development do
112
117
  gem 'rerun'
113
118
  gem 'minitest'
@@ -7,7 +7,7 @@ module Rubee
7
7
  | |_) | | | || _ \| _|
8
8
  | _ <| |__| || |_) | |___
9
9
  |_| \_\\____/ |____/|_____|
10
- Ver: %s
10
+ Ver: %s ...bzzz
11
11
  LOGO
12
12
 
13
13
  class << self
@@ -23,7 +23,7 @@ LOGO
23
23
 
24
24
  port ||= '7000'
25
25
  print_logo
26
- color_puts("Starting takeoff of ruBee server on port #{port}...", color: :yellow)
26
+ color_puts("Starting takeoff of ruBee on port: #{port}...", color: :yellow)
27
27
  command = "#{jit_prefix(jit)}rackup #{ENV['RACKUP_FILE']} -p #{port}"
28
28
  color_puts(command, color: :gray)
29
29
  exec(command)
@@ -9,6 +9,7 @@ module Rubee
9
9
  development: {
10
10
  database_url: '',
11
11
  port: 7000,
12
+ redis_url: '',
12
13
  },
13
14
  production: {},
14
15
  test: {},
@@ -36,6 +37,11 @@ module Rubee
36
37
  @configuraiton[args[:app].to_sym][args[:env].to_sym][:database_url] = args[:url]
37
38
  end
38
39
 
40
+ def redis_url=(args)
41
+ args[:app] ||= :app
42
+ @configuraiton[args[:app].to_sym][args[:env].to_sym][:redis_url] = args[:url]
43
+ end
44
+
39
45
  def async_adapter=(args)
40
46
  args[:app] ||= :app
41
47
  @configuraiton[args[:app].to_sym][args[:env].to_sym][:async_adapter] = args[:async_adapter]
@@ -82,6 +88,16 @@ module Rubee
82
88
  @configuraiton[args[:app].to_sym][ENV['RACK_ENV']&.to_sym || :development][:react] || {}
83
89
  end
84
90
 
91
+ def pubsub_container=(args)
92
+ args[:app] ||= :app
93
+ @configuraiton[args[:app].to_sym][args[:env].to_sym][:pubsub_container] = args[:pubsub_container]
94
+ end
95
+
96
+ def pubsub_container(**args)
97
+ args[:app] ||= :app
98
+ @configuraiton[args[:app].to_sym][ENV['RACK_ENV']&.to_sym || :development][:pubsub_container] || ::Rubee::PubSub::Redis.instance
99
+ end
100
+
85
101
  def method_missing(method_name, *args)
86
102
  return unless method_name.to_s.start_with?('get_')
87
103
 
@@ -51,6 +51,8 @@ module Rubee
51
51
  [status, headers.merge('content-type' => 'application/javascript'), [object]]
52
52
  in :css
53
53
  [status, headers.merge('content-type' => 'text/css'), [object]]
54
+ in :websocket
55
+ object # hash is expected
54
56
  in :file
55
57
  [
56
58
  status,
@@ -101,23 +103,38 @@ module Rubee
101
103
  erb_template.result(binding)
102
104
  end
103
105
 
104
- def params
105
- inputs = @request.env['rack.input'].read
106
- body = begin
107
- JSON.parse(@request.body.read.strip)
108
- rescue StandardError
109
- {}
110
- end
111
- begin
112
- body.merge!(URI.decode_www_form(inputs).to_h.transform_keys(&:to_sym))
113
- rescue StandardError
114
- nil
106
+ def websocket
107
+ action = @params[:action]
108
+ unless ['subscribe', 'unsubscribe', 'publish'].include?(action)
109
+ response_with(object: "Unknown action: #{action}", type: :websocket)
115
110
  end
111
+
112
+ public_send(action)
113
+ end
114
+
115
+ def params
116
+ # Read raw input safely (only once)
117
+ raw_input = @request.body.read.to_s.strip
118
+ @request.body.rewind if @request.body.respond_to?(:rewind)
119
+
120
+ # Try parsing JSON first, fall back to form-encoded data
121
+ parsed_input =
122
+ begin
123
+ JSON.parse(raw_input)
124
+ rescue StandardError
125
+ begin
126
+ URI.decode_www_form(raw_input).to_h.transform_keys(&:to_sym)
127
+ rescue
128
+ {}
129
+ end
130
+ end
131
+
132
+ # Combine route params, request params, and body
116
133
  @params ||= extract_params(@request.path, @route[:path])
117
- .merge(body)
134
+ .merge(parsed_input)
118
135
  .merge(@request.params)
119
136
  .transform_keys(&:to_sym)
120
- .reject { |k, _v| [:_method].include?(k.downcase.to_sym) }
137
+ .reject { |k, _v| k.to_sym == :_method }
121
138
  end
122
139
 
123
140
  def headers
@@ -125,6 +142,10 @@ module Rubee
125
142
  .collect { |key, val| [key.sub(/^HTTP_/, ''), val] }
126
143
  end
127
144
 
145
+ def websocket_connections
146
+ Rubee::WebSocketConnections.instance
147
+ end
148
+
128
149
  def extract_params(path, pattern)
129
150
  regex_pattern = pattern.gsub(/\{(\w+)\}/, '(?<\1>[^/]+)')
130
151
  regex = Regexp.new("^#{regex_pattern}$")
@@ -135,5 +156,26 @@ module Rubee
135
156
 
136
157
  {}
137
158
  end
159
+
160
+ def handle_websocket
161
+ res = Rubee::WebSocket.call(@request.env) do |payload|
162
+ @params = payload
163
+ yield
164
+ end
165
+ res
166
+ end
167
+
168
+ class << self
169
+ def attach_websocket!
170
+ around(
171
+ :websocket, :handle_websocket,
172
+ if: -> do
173
+ redis_available = Rubee::Features.redis_available?
174
+ Rubee::Logger.error(message: 'Please make sure redis server is running') unless redis_available
175
+ redis_available
176
+ end
177
+ )
178
+ end
179
+ end
138
180
  end
139
181
  end
@@ -11,7 +11,7 @@ module Rubee
11
11
  hook = Module.new do
12
12
  define_method(method) do |*args, &block|
13
13
  if conditions_met?(options[:if], options[:unless])
14
- handler.respond_to?(:call) ? handler.call : send(handler)
14
+ handler.respond_to?(:call) ? safe_call(handler, [self, args]) : send(handler)
15
15
  end
16
16
 
17
17
  super(*args, &block)
@@ -29,7 +29,7 @@ module Rubee
29
29
  result = super(*args, &block)
30
30
 
31
31
  if conditions_met?(options[:if], options[:unless])
32
- handler.respond_to?(:call) ? handler.call : send(handler)
32
+ handler.respond_to?(:call) ? safe_call(handler, [self, args]) : send(handler)
33
33
  end
34
34
 
35
35
  result
@@ -47,7 +47,7 @@ module Rubee
47
47
  if conditions_met?(options[:if], options[:unless])
48
48
  if handler.respond_to?(:call)
49
49
  result = nil
50
- handler.call do
50
+ safe_call(handler, [self, args]) do
51
51
  result = super(*args, &block)
52
52
  end
53
53
 
@@ -67,33 +67,74 @@ module Rubee
67
67
  end
68
68
  end
69
69
 
70
- def conditions_met?(if_condition = nil, unless_condition = nil)
70
+ def conditions_met?(if_condition = nil, unless_condition = nil, instance = nil)
71
71
  return true if if_condition.nil? && unless_condition.nil?
72
-
73
72
  if_condition_result =
74
73
  if if_condition.nil?
75
74
  true
76
75
  elsif if_condition.respond_to?(:call)
77
- if_condition.call
78
- elsif respond_to?(if_condition)
79
- send(if_condition)
76
+ safe_call(if_condition, [instance])
77
+ elsif instance.respond_to?(if_condition)
78
+ instance.send(if_condition)
80
79
  end
81
80
  unless_condition_result =
82
81
  if unless_condition.nil?
83
82
  false
84
83
  elsif unless_condition.respond_to?(:call)
85
- unless_condition.call
86
- elsif respond_to?(unless_condition)
87
- send(unless_condition)
84
+ safe_call(unless_condition, [instance])
85
+ elsif instance.respond_to?(unless_condition)
86
+ instance.send(unless_condition)
88
87
  end
89
88
 
90
89
  if_condition_result && !unless_condition_result
91
90
  end
91
+
92
+ def safe_call(handler, call_args = [], &block)
93
+ if handler.is_a?(Proc)
94
+ wrapped = safe_lambda(handler, &block)
95
+
96
+ # Forward block to the handler lambda if present
97
+ if block
98
+ wrapped.call(*call_args, &block)
99
+ else
100
+ wrapped.call(*call_args)
101
+ end
102
+ else
103
+ handler.call
104
+ block&.call
105
+ end
106
+ end
107
+
108
+ def safe_lambda(strict_lambda, &block)
109
+ return strict_lambda unless strict_lambda.is_a?(Proc)
110
+ return strict_lambda unless strict_lambda.lambda?
111
+ return strict_lambda unless strict_lambda.arity >= 0
112
+
113
+ proc do |*call_args|
114
+ lambda_arity = strict_lambda.arity
115
+
116
+ # Take only what lambda can handle, pad missing ones with nil
117
+ args_for_lambda = call_args.first(lambda_arity)
118
+ if args_for_lambda.length < lambda_arity
119
+ args_for_lambda += Array.new(lambda_arity - args_for_lambda.length, nil)
120
+ end
121
+
122
+ strict_lambda.call(*args_for_lambda, &block)
123
+ end
124
+ end
92
125
  end
93
126
 
94
127
  module InstanceMethods
95
128
  def conditions_met?(if_condition = nil, unless_condition = nil)
96
- self.class.conditions_met?(if_condition, unless_condition)
129
+ self.class.conditions_met?(if_condition, unless_condition, self)
130
+ end
131
+
132
+ def safe_lambda(strict_lambda)
133
+ self.class.safe_lambda(strict_lambda)
134
+ end
135
+
136
+ def safe_call(handler, call_args = [], &block)
137
+ self.class.safe_call(handler, call_args, &block)
97
138
  end
98
139
  end
99
140
  end
@@ -20,7 +20,8 @@ module Rubee
20
20
 
21
21
  def to_h
22
22
  instance_variables.each_with_object({}) do |var, hash|
23
- hash[var.to_s.delete('@')] = instance_variable_get(var)
23
+ attr_name = var.to_s.delete('@')
24
+ hash[attr_name] = instance_variable_get(var) unless attr_name.start_with?('__')
24
25
  end
25
26
  end
26
27
  end
@@ -0,0 +1,130 @@
1
+ module Rubee
2
+ module Validatable
3
+ class Error < StandardError; end
4
+
5
+ class State
6
+ attr_accessor :errors, :valid
7
+
8
+ def initialize
9
+ @valid = true
10
+ @errors = {}
11
+ end
12
+
13
+ def add_error(attribute, hash)
14
+ @valid = false
15
+ @errors[attribute] ||= {}
16
+ @errors[attribute].merge!(hash)
17
+ end
18
+
19
+ def has_errors_for?(attribute)
20
+ @errors.key?(attribute)
21
+ end
22
+ end
23
+
24
+ class RuleChain
25
+ attr_reader :instance, :attribute
26
+
27
+ def initialize(instance, attribute, state)
28
+ @instance = instance
29
+ @attribute = attribute
30
+ @state = state
31
+ @optional = false
32
+ end
33
+
34
+ def required(error_hash)
35
+ value = @instance.send(@attribute)
36
+ if value.nil? || (value.respond_to?(:empty?) && value.empty?)
37
+ @state.add_error(@attribute, error_hash)
38
+ end
39
+ self
40
+ end
41
+
42
+ def optional(*)
43
+ @optional = true
44
+ self
45
+ end
46
+
47
+ def type(expected_class, error_hash)
48
+ return self if @state.has_errors_for?(@attribute)
49
+
50
+ value = @instance.send(@attribute)
51
+ return self if @optional && value.nil?
52
+
53
+ unless value.is_a?(expected_class)
54
+ @state.add_error(@attribute, error_hash)
55
+ end
56
+ self
57
+ end
58
+
59
+ def condition(handler, error_message)
60
+ return self if @state.has_errors_for?(@attribute)
61
+ value = @instance.send(@attribute)
62
+ return self if @optional && value.nil?
63
+
64
+ if handler.respond_to?(:call)
65
+ @state.add_error(@attribute, error_message) unless handler.call
66
+ else
67
+ @instance.send(handler)
68
+ end
69
+
70
+ self
71
+ end
72
+ end
73
+
74
+ def self.included(base)
75
+ base.extend(ClassMethods)
76
+ base.prepend(Initializer)
77
+ base.include(InstanceMethods)
78
+ end
79
+
80
+ module Initializer
81
+ def initialize(*)
82
+ @__validation_state = State.new
83
+ super
84
+ run_validations
85
+ end
86
+ end
87
+
88
+ module InstanceMethods
89
+ def valid?
90
+ run_validations
91
+ @__validation_state.valid
92
+ end
93
+
94
+ def invalid?
95
+ !valid?
96
+ end
97
+
98
+ def errors
99
+ run_validations
100
+ @__validation_state.errors
101
+ end
102
+
103
+ def run_validations
104
+ @__validation_state = State.new
105
+ self.class&.validation_block&.call(self)
106
+ end
107
+
108
+ def required(attribute, options)
109
+ error_message = options
110
+ RuleChain.new(self, attribute, @__validation_state).required(error_message)
111
+ end
112
+
113
+ def optional(attribute)
114
+ RuleChain.new(self, attribute, @__validation_state).optional
115
+ end
116
+
117
+ def add_error(attribute, hash)
118
+ @__validation_state.add_error(attribute, hash)
119
+ end
120
+ end
121
+
122
+ module ClassMethods
123
+ attr_reader :validation_block
124
+
125
+ def validate(&block)
126
+ @validation_block = block
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,22 @@
1
+ module Rubee
2
+ class Features
3
+ class << self
4
+ def redis_available?
5
+ require "redis"
6
+ redis_url = Rubee::Configuration.get_redis_url
7
+ redis = redis_url&.empty? ? Redis.new : Redis.new(url: redis_url)
8
+ redis.ping
9
+ true
10
+ rescue LoadError, Redis::CannotConnectError
11
+ false
12
+ end
13
+
14
+ def websocket_available?
15
+ require "websocket"
16
+ true
17
+ rescue LoadError
18
+ false
19
+ end
20
+ end
21
+ end
22
+ end
@@ -8,6 +8,7 @@ module Rubee
8
8
 
9
9
  base.include(Rubee::Hookable)
10
10
  base.include(Rubee::Serializable)
11
+ base.include(Rubee::Validatable)
11
12
  end
12
13
 
13
14
  module ClassMethods
@@ -33,12 +33,11 @@ module Rubee
33
33
 
34
34
  else
35
35
  begin
36
- created_object = self.class.create(args)
36
+ created_id = self.class.dataset.insert(args)
37
37
  rescue StandardError => _e
38
38
  return false
39
39
  end
40
- self.id = created_object.id
41
-
40
+ self.id = created_id
42
41
  end
43
42
  true
44
43
  end
@@ -208,9 +207,9 @@ module Rubee
208
207
  if dataset.columns.include?(:created) && dataset.columns.include?(:updated)
209
208
  attrs.merge!(created: Time.now, updated: Time.now)
210
209
  end
211
-
212
- out_id = Rubee::DBTools.with_retry { dataset.insert(**attrs) }
213
- new(**attrs.merge(id: out_id))
210
+ instance = new(**attrs)
211
+ Rubee::DBTools.with_retry { instance.save }
212
+ instance
214
213
  end
215
214
 
216
215
  def destroy_all(cascade: false)
@@ -225,6 +224,15 @@ module Rubee
225
224
  klass.new(**klass_attributes)
226
225
  end
227
226
  end
227
+
228
+ def validate_before_persist!
229
+ before(:save, proc { |model| raise Rubee::Validatable::Error, model.errors.to_s }, if: :invalid?)
230
+ before(:update, proc do |model, args|
231
+ if (instance = model.class.new(*args)) && instance.invalid?
232
+ raise Rubee::Validatable::Error, instance.errors.to_s
233
+ end
234
+ end)
235
+ end
228
236
  end
229
237
  end
230
238
  end
@@ -0,0 +1,44 @@
1
+ module Rubee
2
+ module PubSub
3
+ class Container
4
+ def pub(*)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ # Container Implementation of sub
9
+ def sub(*)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ # Container Implementation of unsub
14
+ def unsub(*)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ protected
19
+
20
+ def retrieve_klasses(iterable)
21
+ iterable.map { |clazz| turn_to_class(clazz) }
22
+ end
23
+
24
+ def turn_to_class(string)
25
+ string.split('::').inject(Object) { |o, c| o.const_get(c) }
26
+ end
27
+
28
+ def fan_out(clazzes, args, &block)
29
+ mutex = Mutex.new
30
+
31
+ mutex.synchronize do
32
+ clazzes.each do |clazz|
33
+ if block
34
+ block.call(clazz.name, args)
35
+ else
36
+ clazz.on_pub(clazz.name, args)
37
+ end
38
+ end
39
+ true
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end