wonkavision 0.5.11 → 0.6.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.
- data/CHANGELOG.rdoc +3 -0
- data/lib/wonkavision.rb +28 -1
- data/lib/wonkavision/aggregation.rb +21 -0
- data/lib/wonkavision/event_coordinator.rb +19 -7
- data/lib/wonkavision/extensions/symbol.rb +55 -0
- data/lib/wonkavision/facts.rb +27 -0
- data/lib/wonkavision/local_job_queue.rb +28 -0
- data/lib/wonkavision/message_mapper.rb +2 -2
- data/lib/wonkavision/message_mapper/map.rb +60 -8
- data/lib/wonkavision/persistence/mongo.rb +95 -0
- data/lib/wonkavision/plugins.rb +2 -1
- data/lib/wonkavision/plugins/analytics/aggregation.rb +139 -0
- data/lib/wonkavision/plugins/analytics/aggregation/aggregation_spec.rb +53 -0
- data/lib/wonkavision/plugins/analytics/aggregation/attribute.rb +22 -0
- data/lib/wonkavision/plugins/analytics/aggregation/dimension.rb +64 -0
- data/lib/wonkavision/plugins/analytics/aggregation/measure.rb +240 -0
- data/lib/wonkavision/plugins/analytics/cellset.rb +171 -0
- data/lib/wonkavision/plugins/analytics/facts.rb +106 -0
- data/lib/wonkavision/plugins/analytics/handlers/apply_aggregation.rb +35 -0
- data/lib/wonkavision/plugins/analytics/handlers/split_by_aggregation.rb +60 -0
- data/lib/wonkavision/plugins/analytics/member_filter.rb +106 -0
- data/lib/wonkavision/plugins/analytics/mongo.rb +6 -0
- data/lib/wonkavision/plugins/analytics/persistence/hash_store.rb +59 -0
- data/lib/wonkavision/plugins/analytics/persistence/mongo_store.rb +85 -0
- data/lib/wonkavision/plugins/analytics/persistence/store.rb +105 -0
- data/lib/wonkavision/plugins/analytics/query.rb +76 -0
- data/lib/wonkavision/plugins/event_handling.rb +15 -3
- data/lib/wonkavision/version.rb +1 -1
- data/test/aggregation_spec_test.rb +99 -0
- data/test/aggregation_test.rb +170 -0
- data/test/analytics/test_aggregation.rb +78 -0
- data/test/apply_aggregation_test.rb +92 -0
- data/test/attribute_test.rb +26 -0
- data/test/cellset_test.rb +200 -0
- data/test/dimension_test.rb +186 -0
- data/test/facts_test.rb +146 -0
- data/test/hash_store_test.rb +112 -0
- data/test/log/test.log +96844 -0
- data/test/map_test.rb +48 -1
- data/test/measure_test.rb +146 -0
- data/test/member_filter_test.rb +143 -0
- data/test/mongo_store_test.rb +115 -0
- data/test/query_test.rb +106 -0
- data/test/split_by_aggregation_test.rb +114 -0
- data/test/store_test.rb +71 -0
- data/test/symbol_test.rb +62 -0
- data/test/test_activity_models.rb +1 -1
- data/test/test_aggregation.rb +42 -0
- data/test/test_data.tuples +100 -0
- data/test/test_helper.rb +7 -0
- metadata +57 -5
data/CHANGELOG.rdoc
CHANGED
data/lib/wonkavision.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "active_support"
|
3
3
|
require "active_support/hash_with_indifferent_access" unless defined?(HashWithIndifferentAccess)
|
4
|
+
require "active_support/core_ext"
|
4
5
|
|
5
6
|
dir = File.dirname(__FILE__)
|
6
7
|
["support",
|
@@ -9,9 +10,12 @@ dir = File.dirname(__FILE__)
|
|
9
10
|
"event",
|
10
11
|
"event_context",
|
11
12
|
"event_namespace",
|
13
|
+
"local_job_queue",
|
12
14
|
"event_coordinator",
|
13
15
|
"event_binding",
|
14
16
|
"event_handler",
|
17
|
+
"facts",
|
18
|
+
"aggregation",
|
15
19
|
"message_mapper/indifferent_access",
|
16
20
|
"message_mapper/map",
|
17
21
|
"message_mapper",
|
@@ -20,14 +24,33 @@ dir = File.dirname(__FILE__)
|
|
20
24
|
"plugins/business_activity/event_binding",
|
21
25
|
"plugins/business_activity",
|
22
26
|
"plugins/timeline",
|
27
|
+
"extensions/symbol",
|
28
|
+
"plugins/analytics/member_filter",
|
29
|
+
"plugins/analytics/persistence/store",
|
30
|
+
"plugins/analytics/persistence/hash_store",
|
31
|
+
"plugins/analytics/facts",
|
32
|
+
"plugins/analytics/aggregation/aggregation_spec",
|
33
|
+
"plugins/analytics/aggregation/attribute",
|
34
|
+
"plugins/analytics/aggregation/dimension",
|
35
|
+
"plugins/analytics/aggregation/measure",
|
36
|
+
"plugins/analytics/aggregation",
|
37
|
+
"plugins/analytics/cellset",
|
38
|
+
"plugins/analytics/query",
|
23
39
|
"acts_as_oompa_loompa",
|
24
40
|
"persistence/mongo_mapper_adapter",
|
25
|
-
"persistence/mongoid_adapter"
|
41
|
+
"persistence/mongoid_adapter",
|
42
|
+
"plugins/analytics/mongo"
|
43
|
+
].each {|lib|require File.join(dir,'wonkavision',lib)}
|
44
|
+
|
45
|
+
|
46
|
+
|
26
47
|
|
27
48
|
#require File.join(dir,"cubicle","mongo_mapper","aggregate_plugin") if defined?(MongoMapper::Document)
|
28
49
|
|
29
50
|
module Wonkavision
|
30
51
|
|
52
|
+
NaN = 0.0 / 0.0
|
53
|
+
|
31
54
|
# def self.register_cubicle_directory(directory_path, recursive=true)
|
32
55
|
# searcher = "#{recursive ? "*" : "**/*"}.rb"
|
33
56
|
# Dir[File.join(directory_path,searcher)].each {|cubicle| require cubicle}
|
@@ -73,3 +96,7 @@ module Wonkavision
|
|
73
96
|
|
74
97
|
|
75
98
|
end
|
99
|
+
|
100
|
+
#Load event handlers for analytics
|
101
|
+
# dir = File.dirname(__FILE__)
|
102
|
+
Dir[File.join(dir,"wonkavision","plugins/analytics/handlers/**/*.rb")].each {|lib|require lib}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Aggregation
|
3
|
+
|
4
|
+
def self.all
|
5
|
+
Wonkavision::Plugins::Aggregation.all
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.persistence
|
9
|
+
@persistence
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(handler)
|
13
|
+
handler.class_eval do
|
14
|
+
extend Plugins
|
15
|
+
use Plugins::Aggregation
|
16
|
+
end
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,11 +2,13 @@ module Wonkavision
|
|
2
2
|
class EventCoordinator
|
3
3
|
|
4
4
|
attr_reader :root_namespace
|
5
|
+
attr_accessor :broadcast_transport, :job_queue
|
5
6
|
|
6
7
|
def initialize
|
7
8
|
@root_namespace = Wonkavision::EventNamespace.new
|
8
|
-
|
9
|
+
#@lock = Mutex.new
|
9
10
|
#@event_cache = {}
|
11
|
+
|
10
12
|
@incoming_event_filters = []
|
11
13
|
end
|
12
14
|
|
@@ -22,7 +24,7 @@ module Wonkavision
|
|
22
24
|
self.instance_eval(&block)
|
23
25
|
end
|
24
26
|
|
25
|
-
def map
|
27
|
+
def map
|
26
28
|
yield root_namespace if block_given?
|
27
29
|
end
|
28
30
|
|
@@ -33,17 +35,27 @@ module Wonkavision
|
|
33
35
|
end
|
34
36
|
|
35
37
|
def receive_event(event_path, event_data)
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
#@lock.synchronize do
|
39
|
+
#If process_incoming_event returns nil or false, it means a filter chose to abort
|
40
|
+
#the event processing, in which case we'll break for lunch.
|
39
41
|
return unless event_data = process_incoming_event(event_path,event_data)
|
40
42
|
|
41
43
|
event_path = Wonkavision.normalize_event_path(event_path)
|
42
44
|
targets = root_namespace.find_matching_segments(event_path).values
|
43
|
-
#If the event wasn't matched, maybe someone is subscribing to '/*' ?
|
45
|
+
#If the event wasn't matched, maybe someone is subscribing to '/*' ?
|
44
46
|
targets = [root_namespace] if targets.blank?
|
45
47
|
targets.each{|target|target.notify_subscribers(event_data,event_path)}
|
46
|
-
end
|
48
|
+
#end
|
49
|
+
end
|
50
|
+
|
51
|
+
def publish(event_path, event_data)
|
52
|
+
raise "No transport was configured with the EventCoordinator to deliver broadcast messages. Please set Wonkavision.event_coordinator.broadcast_transport = <some transport>." unless broadcast_transport
|
53
|
+
broadcast_transport.publish(event_path, event_data)
|
54
|
+
end
|
55
|
+
|
56
|
+
def submit_job(event_path, event_data)
|
57
|
+
job_queue ? job_queue.publish(event_path,event_data) :
|
58
|
+
receive_event(event_path, event_data)
|
47
59
|
end
|
48
60
|
|
49
61
|
protected
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# This concept is torn from the chest cavity of
|
3
|
+
# jnunemakers plucky library (https://github.com/jnunemaker/plucky/blob/master/lib/plucky/extensions/symbol.rb)
|
4
|
+
module Wonkavision
|
5
|
+
module Extensions
|
6
|
+
module Symbol
|
7
|
+
|
8
|
+
[:key, :caption, :sort].each do |dimension_attribute|
|
9
|
+
define_method(dimension_attribute) do
|
10
|
+
_filter(dimension_attribute, :member_type=>:dimension)
|
11
|
+
end unless method_defined?(dimension_attribute)
|
12
|
+
end
|
13
|
+
|
14
|
+
[:sum, :sum2, :count].each do |measure_attribute|
|
15
|
+
define_method(measure_attribute) do
|
16
|
+
_filter(measure_attribute, :member_type=>:measure)
|
17
|
+
end unless method_defined?(measure_attribute)
|
18
|
+
end
|
19
|
+
|
20
|
+
def[](name)
|
21
|
+
_filter(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(name,*args)
|
25
|
+
_filter(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def _member_type
|
30
|
+
self == :measures ? :measure : :dimension
|
31
|
+
end
|
32
|
+
|
33
|
+
def _is_member_name?
|
34
|
+
[:dimensions,:measures].include?(self) == false
|
35
|
+
end
|
36
|
+
|
37
|
+
def _filter(name, options={})
|
38
|
+
options[:member_type] ||= _member_type
|
39
|
+
if _is_member_name?
|
40
|
+
member_name = self
|
41
|
+
options[:attribute_name] = name
|
42
|
+
else
|
43
|
+
member_name = name
|
44
|
+
end
|
45
|
+
Wonkavision::Analytics::MemberFilter.new(member_name,options)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
class Symbol
|
54
|
+
include Wonkavision::Extensions::Symbol
|
55
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Facts
|
3
|
+
|
4
|
+
def self.persistence
|
5
|
+
@persistence
|
6
|
+
end
|
7
|
+
|
8
|
+
#current only supports :mongo
|
9
|
+
def self.persistence=(backend)
|
10
|
+
case backend
|
11
|
+
when :mongo then require File.dirname(__FILE__) + "/plugins/analytics/mongo"
|
12
|
+
else
|
13
|
+
raise "#{backend} is not a supported back end for Wonkavision analytics"
|
14
|
+
end
|
15
|
+
@persistence = backend
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(facts)
|
19
|
+
facts.class_eval do
|
20
|
+
extend Plugins
|
21
|
+
use Plugins::EventHandling
|
22
|
+
use Plugins::Callbacks
|
23
|
+
use Plugins::Facts
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Wonkavision
|
4
|
+
class LocalJobQueue
|
5
|
+
attr_reader :queue
|
6
|
+
def initialize(options={})
|
7
|
+
worker_count = options[:workers] || 2
|
8
|
+
@queue = Queue.new
|
9
|
+
@workers = []
|
10
|
+
worker_count.times do
|
11
|
+
Thread.new do
|
12
|
+
while true
|
13
|
+
if msg = @queue.pop
|
14
|
+
Wonkavision.event_coordinator.receive_event(msg[0],msg[1])
|
15
|
+
else
|
16
|
+
sleep 0.1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def publish(event_path,event)
|
24
|
+
@queue << [event_path, event]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -11,11 +11,11 @@ module Wonkavision
|
|
11
11
|
MessageMapper.maps[map_name] = block
|
12
12
|
end
|
13
13
|
|
14
|
-
def execute(map,data_source)
|
14
|
+
def execute(map,data_source,options={})
|
15
15
|
map_block = map.kind_of?(Proc) ? map : MessageMapper.maps[map]
|
16
16
|
|
17
17
|
raise "#{map} not found" unless map_block
|
18
|
-
MessageMapper::Map.new.execute(data_source, map_block)
|
18
|
+
MessageMapper::Map.new.execute(data_source, map_block, options)
|
19
19
|
end
|
20
20
|
|
21
21
|
def register_map_directory(directory_path, recursive=true)
|
@@ -5,12 +5,14 @@ module Wonkavision
|
|
5
5
|
include IndifferentAccess
|
6
6
|
|
7
7
|
def initialize(context = nil)
|
8
|
+
@write_nils = true
|
8
9
|
@context_stack = []
|
9
10
|
@context_stack.push(context) if context
|
10
11
|
@formats = default_formats
|
11
12
|
end
|
12
13
|
|
13
|
-
def execute(context,map_block)
|
14
|
+
def execute(context,map_block,options={})
|
15
|
+
@write_nils = options[:write_nils].nil? ? true : options[:write_nils]
|
14
16
|
@context_stack.push(context)
|
15
17
|
instance_eval(&map_block)
|
16
18
|
@context_stack.clear
|
@@ -25,6 +27,14 @@ module Wonkavision
|
|
25
27
|
@context_stack[-1]
|
26
28
|
end
|
27
29
|
|
30
|
+
def ignore_nil!
|
31
|
+
@write_nils = false
|
32
|
+
end
|
33
|
+
|
34
|
+
def write_nil!
|
35
|
+
@write_nils = true
|
36
|
+
end
|
37
|
+
|
28
38
|
def from (context,&block)
|
29
39
|
raise "No block ws provided to 'from'" unless block
|
30
40
|
return if context.nil?
|
@@ -146,7 +156,7 @@ module Wonkavision
|
|
146
156
|
if kind_of?(Date)
|
147
157
|
self
|
148
158
|
elsif respond_to?(:to_date)
|
149
|
-
|
159
|
+
to_date
|
150
160
|
elsif (date_str=to_s) && date_str.length > 0
|
151
161
|
begin
|
152
162
|
Date.parse(date_str)
|
@@ -187,7 +197,45 @@ module Wonkavision
|
|
187
197
|
end
|
188
198
|
end
|
189
199
|
|
200
|
+
def duration(*args, &block)
|
201
|
+
opts = args.extract_options! || {}
|
202
|
+
|
203
|
+
from = opts.delete(:from)
|
204
|
+
to = opts.delete(:to)
|
205
|
+
|
206
|
+
return nil unless from || to
|
207
|
+
|
208
|
+
from ||= Time.now; to ||= Time.now
|
209
|
+
|
210
|
+
unit = opts.delete(:in) || opts.delete(:unit) || :seconds
|
211
|
+
|
212
|
+
duration = convert_seconds(to-from,unit)
|
213
|
+
|
214
|
+
assignment = {args.shift => duration}
|
215
|
+
args << assignment << opts
|
216
|
+
|
217
|
+
value(*args, &block)
|
218
|
+
|
219
|
+
end
|
220
|
+
alias :elapsed :duration
|
221
|
+
|
190
222
|
private
|
223
|
+
|
224
|
+
def convert_seconds(duration, unit)
|
225
|
+
duration /
|
226
|
+
case unit.to_s
|
227
|
+
when "seconds" then 1
|
228
|
+
when "minutes" then 60
|
229
|
+
when "hours" then 60 * 60
|
230
|
+
when "days" then 60 * 60 * 24
|
231
|
+
when "weeks" then 60 * 60 * 24 * 7
|
232
|
+
when "months" then 60 * 60 * 24 * 30
|
233
|
+
when "years" then 60 * 60 * 24 * 365
|
234
|
+
else raise "Cannot convert duration to unknown time unit #{unit}"
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
191
239
|
def format_value(val,opts={})
|
192
240
|
val = opts[:default] || opts[:default_value] if val.nil?
|
193
241
|
return val if val.nil?
|
@@ -202,16 +250,18 @@ module Wonkavision
|
|
202
250
|
end
|
203
251
|
|
204
252
|
def extract_value_from_context(context,field_name,block=nil)
|
205
|
-
|
206
|
-
|
207
|
-
elsif context.respond_to?(:[])
|
253
|
+
value = nil
|
254
|
+
if context.respond_to?(:[])
|
208
255
|
value = context[field_name]
|
209
256
|
if value.nil? && field_name
|
210
257
|
value = context[field_name.to_sym] || context[field_name.to_s]
|
211
258
|
end
|
212
|
-
else
|
213
|
-
value = nil
|
214
259
|
end
|
260
|
+
|
261
|
+
if context.respond_to?(field_name.to_sym)
|
262
|
+
value = context.instance_eval("self.#{field_name}")
|
263
|
+
end unless value
|
264
|
+
|
215
265
|
value = value.instance_eval(&block) if block
|
216
266
|
value
|
217
267
|
end
|
@@ -219,7 +269,9 @@ module Wonkavision
|
|
219
269
|
def set_value(field_name,val,opts={})
|
220
270
|
if prefix = opts[:prefix]; field_name = "#{prefix}#{field_name}"; end
|
221
271
|
if suffix = opts[:suffix]; field_name = "#{field_name}#{suffix}"; end
|
222
|
-
|
272
|
+
unless val.nil? && !@write_nils
|
273
|
+
self[field_name] = format_value(val,opts)
|
274
|
+
end
|
223
275
|
end
|
224
276
|
|
225
277
|
def default_formats
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# Taken almost verbatim from https://github.com/jnunemaker/mongomapper/blob/master/lib/mongo_mapper/connection.rb
|
3
|
+
require 'uri'
|
4
|
+
require 'mongo'
|
5
|
+
|
6
|
+
module Wonkavision
|
7
|
+
module Mongo
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def initialize(connection=nil)
|
11
|
+
@connection = connection
|
12
|
+
end
|
13
|
+
|
14
|
+
# @api public
|
15
|
+
def connection
|
16
|
+
@connection ||= ::Mongo::Connection.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# @api public
|
20
|
+
def connection=(new_connection)
|
21
|
+
@connection = new_connection
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api public
|
25
|
+
def logger
|
26
|
+
connection.logger
|
27
|
+
end
|
28
|
+
|
29
|
+
# @api public
|
30
|
+
def database=(name)
|
31
|
+
@database = nil
|
32
|
+
@database_name = name
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api public
|
36
|
+
def database
|
37
|
+
if @database_name.blank?
|
38
|
+
raise 'You forgot to set the default database name: database = "foobar"'
|
39
|
+
end
|
40
|
+
|
41
|
+
@database ||= connection.db(@database_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def config=(hash)
|
45
|
+
@config = hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def config
|
49
|
+
raise 'Set config before connecting. config = {...}' unless defined?(@config)
|
50
|
+
@config
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def config_for_environment(environment)
|
55
|
+
env = config[environment]
|
56
|
+
return env if env['uri'].blank?
|
57
|
+
|
58
|
+
uri = URI.parse(env['uri'])
|
59
|
+
raise InvalidScheme.new('must be mongodb') unless uri.scheme == 'mongodb'
|
60
|
+
{
|
61
|
+
'host' => uri.host,
|
62
|
+
'port' => uri.port,
|
63
|
+
'database' => uri.path.gsub(/^\//, ''),
|
64
|
+
'username' => uri.user,
|
65
|
+
'password' => uri.password,
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def connect(environment, options={})
|
70
|
+
raise 'Set config before connecting. config = {...}' if config.blank?
|
71
|
+
env = config_for_environment(environment)
|
72
|
+
self.connection = ::Mongo::Connection.new(env['host'], env['port'], options)
|
73
|
+
self.database = env['database']
|
74
|
+
database.authenticate(env['username'], env['password']) if env['username'] && env['password']
|
75
|
+
end
|
76
|
+
|
77
|
+
def setup(config, environment, options={})
|
78
|
+
handle_passenger_forking
|
79
|
+
self.config = config
|
80
|
+
connect(environment, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
def handle_passenger_forking
|
84
|
+
if defined?(PhusionPassenger)
|
85
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
86
|
+
connection.connect if forked
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Connection
|
92
|
+
include Wonkavision::Mongo
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|