wonkavision 0.5.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/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
|