wonkavision 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +3 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +0 -0
- data/Rakefile +48 -0
- data/lib/wonkavision.rb +68 -0
- data/lib/wonkavision/acts_as_oompa_loompa.rb +22 -0
- data/lib/wonkavision/event.rb +33 -0
- data/lib/wonkavision/event_binding.rb +21 -0
- data/lib/wonkavision/event_coordinator.rb +75 -0
- data/lib/wonkavision/event_handler.rb +14 -0
- data/lib/wonkavision/event_namespace.rb +72 -0
- data/lib/wonkavision/event_path_segment.rb +37 -0
- data/lib/wonkavision/message_mapper.rb +30 -0
- data/lib/wonkavision/message_mapper/indifferent_access.rb +26 -0
- data/lib/wonkavision/message_mapper/map.rb +126 -0
- data/lib/wonkavision/persistence/mongo_mapper_adapter.rb +33 -0
- data/lib/wonkavision/plugins.rb +30 -0
- data/lib/wonkavision/plugins/business_activity.rb +79 -0
- data/lib/wonkavision/plugins/event_handling.rb +78 -0
- data/lib/wonkavision/plugins/timeline.rb +80 -0
- data/lib/wonkavision/support.rb +0 -0
- data/lib/wonkavision/version.rb +3 -0
- data/test/business_activity_test.rb +24 -0
- data/test/config/database.yml +15 -0
- data/test/event_coordinator_test.rb +72 -0
- data/test/event_handler_test.rb +47 -0
- data/test/event_namespace_test.rb +109 -0
- data/test/event_path_segment_test.rb +56 -0
- data/test/event_test.rb +54 -0
- data/test/log/test.log +7853 -0
- data/test/map_test.rb +187 -0
- data/test/message_mapper_test.rb +21 -0
- data/test/test_activity_models.rb +56 -0
- data/test/test_helper.rb +63 -0
- data/test/timeline_test.rb +61 -0
- data/test/wonkavision_test.rb +10 -0
- data/wonkavision.gemspec +94 -0
- metadata +145 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module MessageMapper
|
3
|
+
module IndifferentAccess
|
4
|
+
def [](key)
|
5
|
+
key = key.to_s
|
6
|
+
super(key)
|
7
|
+
end
|
8
|
+
|
9
|
+
def []=(key,val)
|
10
|
+
super(key.to_s,val)
|
11
|
+
end
|
12
|
+
|
13
|
+
def include?(key)
|
14
|
+
super(key.to_s)
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(sym,*args,&block)
|
18
|
+
return self[sym.to_s[0..-2]] = args[0] if sym.to_s =~ /.*=$/
|
19
|
+
return self[sym] if self.keys.include?(sym.to_s)
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
|
3
|
+
module MessageMapper
|
4
|
+
class Map < Hash
|
5
|
+
include IndifferentAccess
|
6
|
+
|
7
|
+
def initialize(context)
|
8
|
+
@context_stack = []
|
9
|
+
@context_stack.push(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
def context
|
15
|
+
@context_stack[-1]
|
16
|
+
end
|
17
|
+
|
18
|
+
def from (context,&block)
|
19
|
+
raise "No block ws provided to 'from'" unless block
|
20
|
+
@context_stack.push(context)
|
21
|
+
instance_eval(&block)
|
22
|
+
@context_stack.pop
|
23
|
+
end
|
24
|
+
|
25
|
+
def exec(map_name)
|
26
|
+
mapped = MessageMapper.execute(map_name,context)
|
27
|
+
self.merge!(mapped) if mapped
|
28
|
+
end
|
29
|
+
alias import exec
|
30
|
+
|
31
|
+
def map(source,options={},&block)
|
32
|
+
raise "Neither a block nor a map_name were provided to 'map'" unless (block || options.keys.include?(:map_name))
|
33
|
+
if (source.is_a?(Hash))
|
34
|
+
field_name = source.keys[0]
|
35
|
+
ctx = source[field_name]
|
36
|
+
else
|
37
|
+
field_name = source
|
38
|
+
ctx = context.instance_eval("self.#{field_name}")
|
39
|
+
end
|
40
|
+
if ctx
|
41
|
+
if (map_name = options.delete(:map_name))
|
42
|
+
child = MessageMapper.execute(map_name,ctx)
|
43
|
+
else
|
44
|
+
child = Map.new(ctx)
|
45
|
+
child.instance_eval(&block)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
child = {}
|
49
|
+
end
|
50
|
+
self[field_name] = child
|
51
|
+
end
|
52
|
+
|
53
|
+
def string(*args)
|
54
|
+
value(*args) {to_s}
|
55
|
+
end
|
56
|
+
|
57
|
+
def float(*args)
|
58
|
+
value(*args){respond_to?(:to_f) ? to_f : self}
|
59
|
+
end
|
60
|
+
|
61
|
+
def iso8601(*args)
|
62
|
+
value(*args) do
|
63
|
+
if respond_to?(:strftime)
|
64
|
+
strftime("%Y-%m-%dT%H:%M:%S")
|
65
|
+
elsif respond_to?(:ToString)
|
66
|
+
ToString("yyyy-MM-ddTHH:mm:ss")
|
67
|
+
else
|
68
|
+
self
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def date(*args)
|
74
|
+
value(*args) do
|
75
|
+
if kind_of?(Time) || kind_of?(Date)
|
76
|
+
self
|
77
|
+
elsif respond_to?(:to_time)
|
78
|
+
to_time
|
79
|
+
elsif respond_to?(:to_date)
|
80
|
+
to_date
|
81
|
+
elsif (date_str=to_s) && date_str.length > 0
|
82
|
+
begin
|
83
|
+
Time.parse(date_str)
|
84
|
+
rescue
|
85
|
+
self
|
86
|
+
end
|
87
|
+
else
|
88
|
+
self
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def boolean(*args)
|
94
|
+
value(*args) do
|
95
|
+
%w(true yes).include?(to_s.downcase) ? true : false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def int(*args)
|
100
|
+
value(*args){respond_to?(:to_i) ? to_i : self}
|
101
|
+
end
|
102
|
+
|
103
|
+
def value(*args,&block)
|
104
|
+
if (args.length == 1 && args[0].is_a?(Hash))
|
105
|
+
args[0].each do |key,value|
|
106
|
+
value = context.instance_eval(&value) if value.is_a?(Proc)
|
107
|
+
value = value.instance_eval(&block) if block
|
108
|
+
self[key] = value
|
109
|
+
end
|
110
|
+
else
|
111
|
+
args.each do |field_name|
|
112
|
+
if context.respond_to?(field_name.to_sym)
|
113
|
+
value = context.instance_eval("self.#{field_name}")
|
114
|
+
elsif context.respond_to?(:[])
|
115
|
+
value = context[field_name]
|
116
|
+
else
|
117
|
+
value = nil
|
118
|
+
end
|
119
|
+
value = value.instance_eval(&block) if block
|
120
|
+
self[field_name] = value
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Persistence
|
3
|
+
module MongoMapperAdapter
|
4
|
+
|
5
|
+
def self.included(model)
|
6
|
+
model.plugin Wonkavision::Persistence::MongoMapperAdapter
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
include Wonkavision::ActsAsOompaLoompa
|
11
|
+
|
12
|
+
def define_document_key(key_name,key_type,options={})
|
13
|
+
key(key_name, key_type, options) unless keys[key_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def update_activity(activity,event_data)
|
17
|
+
activity.assign(event_data)
|
18
|
+
:updated
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_activity_instance(correlation_field_name,correlation_id)
|
22
|
+
self.send("find_or_create_by_#{correlation_field_name}",correlation_id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if defined?(::MongoMapper::Document)
|
32
|
+
MongoMapper::Document.append_inclusions(Wonkavision::Persistence::MongoMapperAdapter)
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#This concept (and code) shamelessly stolen from MongoMapper
|
2
|
+
#(http://railstips.org/blog/archives/2010/02/21/mongomapper-07-plugins/)
|
3
|
+
#as so much of my Ruby code tends to be. I added a few little things and changed some
|
4
|
+
#names to avoid conflicts.
|
5
|
+
module Wonkavision
|
6
|
+
module Plugins
|
7
|
+
def wonkavision_plugins
|
8
|
+
@wonkavision_plugins ||= []
|
9
|
+
end
|
10
|
+
|
11
|
+
def has_wonkavision_plugin?(plugin)
|
12
|
+
wonkavision_plugins.detect{|p|p==plugin}
|
13
|
+
end
|
14
|
+
|
15
|
+
def ensure_wonkavision_plugin(plugin,option={})
|
16
|
+
use(plugin,options) unless has_wonkavision_plugin?(plugin)
|
17
|
+
end
|
18
|
+
|
19
|
+
def plug(mod,options={})
|
20
|
+
extend mod::ClassMethods if mod.const_defined?(:ClassMethods)
|
21
|
+
include mod::InstanceMethods if mod.const_defined?(:InstanceMethods)
|
22
|
+
extend mod::Fields if mod.const_defined?(:Fields)
|
23
|
+
include mod::Fields if mod.const_defined?(:Fields)
|
24
|
+
mod.configure(self,options) if mod.respond_to?(:configure)
|
25
|
+
wonkavision_plugins << mod
|
26
|
+
end
|
27
|
+
alias use plug
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Plugins
|
3
|
+
module BusinessActivity
|
4
|
+
|
5
|
+
def self.all
|
6
|
+
@@all ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.configure(activity,options={})
|
10
|
+
activity.write_inheritable_attribute :business_activity_options, {}
|
11
|
+
activity.class_inheritable_reader :business_activity_options
|
12
|
+
|
13
|
+
activity.write_inheritable_attribute :correlation_ids, []
|
14
|
+
activity.class_inheritable_reader :correlation_ids
|
15
|
+
|
16
|
+
BusinessActivity.all << activity
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
def event(name,*args,&block)
|
22
|
+
handle(name,args) do |data,path|
|
23
|
+
activity = find_activity(data)
|
24
|
+
result = :ok
|
25
|
+
if (block_given?)
|
26
|
+
result = case block.arity
|
27
|
+
when 3 then yield activity,data,path
|
28
|
+
when 2 then yield activity,data
|
29
|
+
when 1 then yield activity
|
30
|
+
else yield
|
31
|
+
end
|
32
|
+
end
|
33
|
+
unless result == :handled
|
34
|
+
result = update_activity(activity,data) unless result == :updated
|
35
|
+
activity.save!
|
36
|
+
end
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def correlate_by(*args)
|
42
|
+
return {:model=>correlation_id_field, :event=>event_correlation_id_key} if args.blank?
|
43
|
+
model_field,event_field = if args.length == 1 then
|
44
|
+
case args[0]
|
45
|
+
when Hash then [args[0][:model], args[0][:event] || args[0][:model]]
|
46
|
+
else [args[0],args[0]]
|
47
|
+
end
|
48
|
+
else
|
49
|
+
[args[0],args[1] || args[0]]
|
50
|
+
end
|
51
|
+
|
52
|
+
business_activity_options[:correlation_id_field] = model_field
|
53
|
+
business_activity_options[:event_correlation_id_key] = event_field
|
54
|
+
|
55
|
+
define_document_key correlation_id_field, String, :index=>true
|
56
|
+
correlation_ids << {:model=>model_field.to_s, :event=>event_field.to_s}
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_activity(event_data)
|
60
|
+
correlation_id = event_data[event_correlation_id_key.to_s]
|
61
|
+
find_activity_instance(correlation_id_field,correlation_id)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
module Fields
|
67
|
+
|
68
|
+
def correlation_id_field
|
69
|
+
business_activity_options[:correlation_id_field] ||= "id"
|
70
|
+
end
|
71
|
+
|
72
|
+
def event_correlation_id_key
|
73
|
+
business_activity_options[:event_correlation_id_key] || correlation_id_field
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Plugins
|
3
|
+
module EventHandling
|
4
|
+
|
5
|
+
def self.configure(handler,options)
|
6
|
+
handler.write_inheritable_attribute :event_handler_options, {}
|
7
|
+
handler.class_inheritable_reader :event_handler_options
|
8
|
+
|
9
|
+
handler.write_inheritable_attribute :bindings, []
|
10
|
+
handler.class_inheritable_reader :bindings
|
11
|
+
|
12
|
+
handler.write_inheritable_attribute :maps, []
|
13
|
+
handler.class_inheritable_reader :maps
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def options
|
18
|
+
event_handler_options
|
19
|
+
end
|
20
|
+
|
21
|
+
def event_path(event_name)
|
22
|
+
return event_name.to_s if Wonkavision.is_absolute_path(event_name) #don't mess with an absolute path
|
23
|
+
Wonkavision.join(event_namespace,event_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def event_namespace(namespace=nil)
|
27
|
+
return event_handler_options[:event_namespace] unless namespace
|
28
|
+
event_handler_options[:event_namespace] = namespace
|
29
|
+
end
|
30
|
+
alias :namespace :event_namespace
|
31
|
+
|
32
|
+
def map(condition = nil,&block)
|
33
|
+
maps << [condition,block]
|
34
|
+
end
|
35
|
+
|
36
|
+
def handle(name,*args,&block)
|
37
|
+
binding = Wonkavision::EventBinding.new(name,self,*args)
|
38
|
+
binding.subscribe_to_events do |event_data,event_path|
|
39
|
+
event_data = map_data(event_data,event_path)
|
40
|
+
if block_given?
|
41
|
+
case block.arity
|
42
|
+
when 2 then yield event_data,event_path
|
43
|
+
when 1 then yield event_data
|
44
|
+
else yield
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
bindings << binding
|
49
|
+
binding
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def map_data(data,path)
|
55
|
+
maps.each do |map_def|
|
56
|
+
condition = map_def[0]
|
57
|
+
map_block = map_def[1]
|
58
|
+
return Wonkavision::MessageMapper.execute(map_block,data) if map?(condition,data,path)
|
59
|
+
end
|
60
|
+
data.is_a?(Hash) ? data.dup : data
|
61
|
+
end
|
62
|
+
|
63
|
+
def map?(condition,data,path)
|
64
|
+
return true unless condition && condition.to_s != 'all'
|
65
|
+
return path =~ condition if condition.is_a?(Regexp)
|
66
|
+
if (condition.is_a?(Proc))
|
67
|
+
return condition.call if condition.arity <= 0
|
68
|
+
return condition.call(path) if condition.arity == 1
|
69
|
+
return condition.call(path,data)
|
70
|
+
end
|
71
|
+
#default behavior
|
72
|
+
header.properties[:routing_key] == filter.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
#Code here
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Wonkavision
|
2
|
+
module Plugins
|
3
|
+
module Timeline
|
4
|
+
def self.all
|
5
|
+
@@all ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.configure(activity,options={})
|
9
|
+
activity.ensure_wonkavision_plugin(Wonkavision::Plugins::BusinessActivity,options)
|
10
|
+
activity.write_inheritable_attribute :timeline_milestones, []
|
11
|
+
activity.class_inheritable_reader :timeline_milestones
|
12
|
+
|
13
|
+
options = {
|
14
|
+
:timeline_field => "timeline",
|
15
|
+
:latest_milestone_field => "latest_milestone",
|
16
|
+
:event_time_key => "event_time"
|
17
|
+
}.merge(options)
|
18
|
+
|
19
|
+
activity.business_activity_options.merge!(options)
|
20
|
+
|
21
|
+
activity.define_document_key(activity.timeline_field,Hash,:default=>{})
|
22
|
+
activity.define_document_key(activity.latest_milestone_field, String, :default=>"awaiting_first_event")
|
23
|
+
|
24
|
+
Timeline.all << activity
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
def milestone(name,*args)
|
30
|
+
timeline_milestones << event(name,*args) do |activity,event_data,event_path|
|
31
|
+
event_time = extract_event_time(event_data,event_path)
|
32
|
+
prev_event_time = activity[timeline_field][name]
|
33
|
+
unless prev_event_time
|
34
|
+
activity[timeline_field][name] = event_time
|
35
|
+
#If the event being processed happened earlier than a previously
|
36
|
+
#recorded event, we don't want to overwrite state of the activity, as
|
37
|
+
#it is already more up to date than the incoming event.
|
38
|
+
latest_ms = activity[latest_milestone_field]
|
39
|
+
unless latest_ms &&
|
40
|
+
(last_event = activity[timeline_field][latest_ms]) &&
|
41
|
+
last_event > event_time
|
42
|
+
update_activity(activity,event_data)
|
43
|
+
activity[latest_milestone_field] = name
|
44
|
+
end
|
45
|
+
:updated
|
46
|
+
else
|
47
|
+
:handled #If there was a previous event time for this milestone, we will just skip this event
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def convert_time(time)
|
53
|
+
if (time.is_a?(Hash)) && (time.keys.include?(:date) || time.keys.include?(:time))
|
54
|
+
time = "#{time[:date]} #{time[:time]}".strip
|
55
|
+
end
|
56
|
+
time ? time.to_time : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def extract_event_time(event_data,event_path)
|
61
|
+
convert_time(event_data.delete(event_time_key.to_s)) || Time.now.utc
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module Fields
|
66
|
+
def timeline_field
|
67
|
+
business_activity_options[:timeline_field]
|
68
|
+
end
|
69
|
+
|
70
|
+
def latest_milestone_field
|
71
|
+
business_activity_options[:latest_milestone_field]
|
72
|
+
end
|
73
|
+
|
74
|
+
def event_time_key
|
75
|
+
business_activity_options[:event_time_key]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|