replay 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rvmrc +1 -1
- data/Gemfile +7 -3
- data/Guardfile +10 -0
- data/LICENSE +21 -0
- data/README.md +153 -0
- data/Rakefile +15 -0
- data/lib/replay.rb +39 -10
- data/lib/replay/backends.rb +50 -0
- data/lib/replay/event_declarations.rb +36 -0
- data/lib/replay/event_decorator.rb +13 -0
- data/lib/replay/events.rb +24 -0
- data/lib/replay/inflector.rb +55 -0
- data/lib/replay/observer.rb +18 -0
- data/lib/replay/publisher.rb +72 -0
- data/lib/replay/repository.rb +61 -0
- data/lib/replay/repository/configuration.rb +30 -0
- data/lib/replay/repository/identity_map.rb +25 -0
- data/lib/replay/router.rb +5 -0
- data/lib/replay/router/default_router.rb +21 -0
- data/lib/replay/rspec.rb +50 -0
- data/lib/replay/subscription_manager.rb +28 -0
- data/lib/replay/test.rb +64 -0
- data/lib/replay/test/test_event_stream.rb +19 -0
- data/lib/replay/version.rb +1 -1
- data/proofs/all.rb +7 -0
- data/proofs/proofs_init.rb +10 -0
- data/proofs/replay/inflector_proof.rb +32 -0
- data/proofs/replay/publisher_proof.rb +170 -0
- data/proofs/replay/repository_configuration_proof.rb +67 -0
- data/proofs/replay/repository_proof.rb +46 -0
- data/proofs/replay/subscriber_manager_proof.rb +39 -0
- data/proofs/replay/test_proof.rb +28 -0
- data/replay.gemspec +5 -4
- data/test/replay/observer_spec.rb +37 -0
- data/test/replay/router/default_router_spec.rb +43 -0
- data/test/test_helper.rb +10 -0
- metadata +65 -48
- data/README +0 -27
- data/lib/replay/active_record_event_store.rb +0 -32
- data/lib/replay/domain.rb +0 -33
- data/lib/replay/event.rb +0 -27
- data/lib/replay/event_store.rb +0 -55
- data/lib/replay/projector.rb +0 -19
- data/lib/replay/test_storage.rb +0 -8
- data/lib/replay/unknown_event_error.rb +0 -2
- data/test/spec_helper.rb +0 -6
- data/test/test_events.sqlite3 +0 -0
- data/test/unit/active_record_event_store_spec.rb +0 -24
- data/test/unit/domain_spec.rb +0 -53
- data/test/unit/event_spec.rb +0 -13
- data/test/unit/event_store_spec.rb +0 -28
- data/test/unit/projector_spec.rb +0 -19
@@ -0,0 +1,18 @@
|
|
1
|
+
module Replay
|
2
|
+
module Observer
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
class << base
|
6
|
+
def observe(event_type, &block)
|
7
|
+
@observer_blocks ||= Hash.new
|
8
|
+
@observer_blocks[Replay::Inflector.underscore(event_type.to_s)] = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def published(stream_id, event)
|
12
|
+
blk = @observer_blocks[Replay::Inflector.underscore(event.class.to_s)]
|
13
|
+
blk.call(stream_id, event, binding) if blk
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Replay
|
2
|
+
module Publisher
|
3
|
+
def self.included(base)
|
4
|
+
include_essentials base
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.include_essentials(base)
|
8
|
+
base.instance_variable_set :@application_blocks, {}
|
9
|
+
base.extend ClassMethods
|
10
|
+
base.extend(Replay::Events)
|
11
|
+
end
|
12
|
+
|
13
|
+
def subscription_manager
|
14
|
+
@subscription_manager ||= Replay::SubscriptionManager.new(Replay.logger)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_subscriber(subscriber)
|
18
|
+
subscription_manager.add_subscriber(subscriber)
|
19
|
+
end
|
20
|
+
|
21
|
+
def apply(events, raise_unhandled = true)
|
22
|
+
return apply([events], raise_unhandled) unless events.is_a?(Array)
|
23
|
+
|
24
|
+
events.each do |event|
|
25
|
+
blk = block_for(event.class)
|
26
|
+
raise UnhandledEventError.new "event #{event.class.name} is not handled by #{self.class.name}" if (blk.nil? && raise_unhandled)
|
27
|
+
self.instance_exec(event, &blk)
|
28
|
+
end
|
29
|
+
return self
|
30
|
+
end
|
31
|
+
|
32
|
+
def block_for(event_type)
|
33
|
+
self.class.block_for(event_type)
|
34
|
+
end
|
35
|
+
protected :block_for
|
36
|
+
|
37
|
+
def publish(event)
|
38
|
+
apply(event)
|
39
|
+
subscription_manager.notify_subscribers(to_stream_id, event)
|
40
|
+
return self
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def to_stream_id
|
45
|
+
raise Replay::UndefinedKeyError.new("No key attribute defined for #{self.class.to_s}") unless self.class.key_attr
|
46
|
+
self.send(self.class.key_attr).to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
module ClassMethods
|
50
|
+
def key(keysym)
|
51
|
+
@primary_key_method = keysym
|
52
|
+
end
|
53
|
+
|
54
|
+
def key_attr
|
55
|
+
@primary_key_method
|
56
|
+
end
|
57
|
+
|
58
|
+
def apply(event_type, &block)
|
59
|
+
@application_blocks[stringify_class(event_type)] = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def block_for(event_type)
|
63
|
+
blk = @application_blocks[stringify_class(event_type)]
|
64
|
+
return blk
|
65
|
+
end
|
66
|
+
|
67
|
+
def stringify_class(klass)
|
68
|
+
Replay::Inflector.underscore(klass.to_s.dup)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Replay
|
2
|
+
module Repository
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def configuration
|
9
|
+
@configuration ||= Configuration.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# load will always return an initialized instance of the supplied class (unless it doesn't!). if the given
|
13
|
+
# stream has no events (e.g. is not found, new object, etc), load will attempt to call
|
14
|
+
# create on the newly initalized instance of klass
|
15
|
+
#
|
16
|
+
#options:
|
17
|
+
# :create => true #if false, do not call create on this instance if no stream is found
|
18
|
+
def load(klass, stream_id, options={})
|
19
|
+
repository_load(klass, stream_id, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def repository_load(klass, stream_id, options={})
|
23
|
+
events = store.event_stream(stream_id)
|
24
|
+
if events.empty?
|
25
|
+
raise Errors::EventStreamNotFoundError.new("Could not find any events for stream identifier #{stream_id}") if options[:create].nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
obj = prepare(klass.new)
|
29
|
+
obj.create(stream_id) if options[:create] && events.empty?
|
30
|
+
obj.apply(events)
|
31
|
+
|
32
|
+
obj
|
33
|
+
end
|
34
|
+
|
35
|
+
#refresh reloads the object from the data store
|
36
|
+
#naive implementation is just a reload. Once deltas are in place
|
37
|
+
#it can just apply the delta events to the object
|
38
|
+
def self.refresh(obj)
|
39
|
+
new_obj = load(obj.class, obj.to_key)
|
40
|
+
new_obj
|
41
|
+
end
|
42
|
+
|
43
|
+
def prepare(obj)
|
44
|
+
@configuration.subscribers.each do |subscriber|
|
45
|
+
obj.add_subscriber(subscriber)
|
46
|
+
end
|
47
|
+
obj
|
48
|
+
end
|
49
|
+
|
50
|
+
def configure
|
51
|
+
@configuration ||= Configuration.new
|
52
|
+
yield @configuration
|
53
|
+
@configuration
|
54
|
+
end
|
55
|
+
|
56
|
+
def store
|
57
|
+
@configuration.store
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Replay
|
2
|
+
module Repository
|
3
|
+
class Configuration
|
4
|
+
def initialize
|
5
|
+
@default_subscribers =[]
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_default_subscriber(subscriber)
|
9
|
+
subscriber = Replay::Backends.resolve(subscriber) if subscriber.is_a?(String) || subscriber.is_a?(Symbol)
|
10
|
+
@default_subscribers << subscriber
|
11
|
+
end
|
12
|
+
|
13
|
+
def subscribers
|
14
|
+
@default_subscribers
|
15
|
+
end
|
16
|
+
|
17
|
+
def store=(store)
|
18
|
+
store = Replay::Backends.resolve(store)
|
19
|
+
raise Replay::InvalidStorageError.new(store) unless store.respond_to?(:event_stream)
|
20
|
+
raise Replay::InvalidSubscriberError.new(store) unless store.respond_to?(:published)
|
21
|
+
@store = store
|
22
|
+
add_default_subscriber(@store)
|
23
|
+
end
|
24
|
+
|
25
|
+
def store
|
26
|
+
@store
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Replay
|
2
|
+
module Repository
|
3
|
+
module IdentityMap
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
module ClassMethods
|
8
|
+
def load(klass, stream_id, options={})
|
9
|
+
#implement an identity map
|
10
|
+
@_identities ||= {}
|
11
|
+
return @_identities[[klass,stream_id]] if @_identities[[klass, stream_id]]
|
12
|
+
|
13
|
+
obj=repository_load(klass, stream_id, options)
|
14
|
+
@_identities[[klass, stream_id]] = obj
|
15
|
+
|
16
|
+
obj
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear_identity_map
|
20
|
+
@_identities = {}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Replay
|
2
|
+
module Router
|
3
|
+
class DefaultRouter
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@subscription_manager = Replay::SubscriptionManager.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_observer(observer, *events)
|
11
|
+
@subscription_manager.add_subscriber(observer)
|
12
|
+
end
|
13
|
+
|
14
|
+
def published(stream_id, event)
|
15
|
+
@subscription_manager.notify_subscribers(stream_id, event)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Replay::Backends.register(:replay_router, Replay::Router::DefaultRouter)
|
data/lib/replay/rspec.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'replay/test'
|
2
|
+
|
3
|
+
RSpec::Matchers.define :publish do |expected_event|
|
4
|
+
match do |proc_or_obj|
|
5
|
+
if proc_or_obj.respond_to? :call
|
6
|
+
@result = proc_or_obj.call
|
7
|
+
@result.published?(expected_event, @fuzzy)
|
8
|
+
else
|
9
|
+
proc_or_obj.published?(expected_event, @fuzzy)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
chain :fuzzy do
|
14
|
+
@fuzzy = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def failure_message(expected_event, actual, should = true)
|
18
|
+
actual = @result if actual.is_a? Proc
|
19
|
+
|
20
|
+
str = "expected that #{domain_obj_interpretation(actual)} would#{should ? ' ': ' not'} generate #{@fuzzy ? 'an event like' : 'the event'} #{event_interpretation(expected_event)}"
|
21
|
+
similar = actual.similar_events(expected_event)
|
22
|
+
if similar.empty?
|
23
|
+
str += "\nNo similar events found."
|
24
|
+
else
|
25
|
+
str += "\nThe following events matched type, but not attributes:\n#{similar.map{|s| event_interpretation(s)+"\n"}.join("\t\t")}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
failure_message_for_should_not do |actual|
|
29
|
+
failure_message(expected_event, actual, false)
|
30
|
+
end
|
31
|
+
|
32
|
+
failure_message_for_should do |actual|
|
33
|
+
failure_message(expected_event, actual )
|
34
|
+
end
|
35
|
+
|
36
|
+
def domain_obj_interpretation(obj)
|
37
|
+
if obj.respond_to?(:call) && obj.kind_of?(Proc)
|
38
|
+
"block"
|
39
|
+
else
|
40
|
+
obj.class.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def event_interpretation(event)
|
46
|
+
"#{event.class.to_s} [#{event.attributes.reject{|k,v| v.nil?}.keys.map{|k| "#{k.to_s} = #{event.attributes[k]}"}.join(", ")}]"
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Replay
|
2
|
+
class SubscriptionManager
|
3
|
+
|
4
|
+
def initialize(logger = nil)
|
5
|
+
@subscribers = []
|
6
|
+
@logger = logger
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_subscriber(subscriber)
|
10
|
+
if subscriber.respond_to?(:published)
|
11
|
+
@subscribers << subscriber
|
12
|
+
else
|
13
|
+
raise Replay::InvalidSubscriberError.new(subscriber)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def notify_subscribers(stream_id, event)
|
18
|
+
@subscribers.each do |sub|
|
19
|
+
begin
|
20
|
+
sub.published(stream_id, event)
|
21
|
+
rescue Exception => e
|
22
|
+
#hmmmm
|
23
|
+
@logger.error "exception in event subscriber #{sub.class.to_s} while handling event stream #{stream_id} #{event.inspect}: #{e.message}\n#{e.backtrace.join("\n")}" if @logger
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/replay/test.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'replay'
|
2
|
+
|
3
|
+
module Replay::EventExaminer
|
4
|
+
def events
|
5
|
+
@_events ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def published?(event, fuzzy=false)
|
9
|
+
if fuzzy
|
10
|
+
!(events.detect{|e| event.kind_of_matches?(e) }.nil?)
|
11
|
+
else
|
12
|
+
events.detect{|e| event.is_a?(Class) ? e.class == event : e == event}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def similar_events(event)
|
17
|
+
events.select{|e| e.class == event.class}
|
18
|
+
end
|
19
|
+
|
20
|
+
def apply(events, raise_unhandled = true)
|
21
|
+
return apply([events], raise_unhandled) unless events.is_a?(Array)
|
22
|
+
retval = super(events, raise_unhandled)
|
23
|
+
events.each do |event|
|
24
|
+
self.events << event
|
25
|
+
end
|
26
|
+
return retval
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_subscriber?(subscriber)
|
30
|
+
@subscription_manager.has_subscriber?(subscriber)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Replay::SubscriptionManager.class_exec do
|
35
|
+
def has_subscriber?(subscriber)
|
36
|
+
@subscribers.include?(subscriber)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Replay::Publisher::ClassMethods.module_exec do
|
41
|
+
def self.extended(base)
|
42
|
+
@publishers ||= []
|
43
|
+
@publishers << base
|
44
|
+
base.send(:include, Replay::EventExaminer)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Replay::EventDecorator.module_exec do
|
49
|
+
#receiver's non-nil values are a subset of parameters non-nil values
|
50
|
+
def kind_of_matches?(event)
|
51
|
+
relevant_attrs_match = event.attributes.reject{|k,v| v.nil?}
|
52
|
+
relevant_attrs_self = self.attributes.reject{|k,v| v.nil?}
|
53
|
+
|
54
|
+
if (relevant_attrs_self.keys - relevant_attrs_match.keys).empty?
|
55
|
+
if relevant_attrs_self.reject{|k, v| event[k] == v}.any?
|
56
|
+
return false
|
57
|
+
else
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
else
|
61
|
+
return false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Replay
|
2
|
+
class TestEventStream
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@events = []
|
6
|
+
end
|
7
|
+
def publish(stream_id, event)
|
8
|
+
@events << {stream: stream_id, event: event}
|
9
|
+
end
|
10
|
+
|
11
|
+
def published_event?(event)
|
12
|
+
@events.detect{|e| e[:event]==event}
|
13
|
+
end
|
14
|
+
|
15
|
+
def published?(stream_id, event)
|
16
|
+
@events.detect{|e| e == {stream: stream_id, event: event}}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/replay/version.rb
CHANGED
data/proofs/all.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative "../proofs_init.rb"
|
2
|
+
require 'replay/inflector'
|
3
|
+
|
4
|
+
module Replay::Inflector::Proof
|
5
|
+
def camelizes?(flat, cameld)
|
6
|
+
Replay::Inflector.camelize(flat) == cameld
|
7
|
+
end
|
8
|
+
def underscores?(from, to)
|
9
|
+
Replay::Inflector.underscore(from) == to
|
10
|
+
end
|
11
|
+
def constantizes?(classname, expected_constant)
|
12
|
+
Replay::Inflector.constantize(classname) == expected_constant
|
13
|
+
end
|
14
|
+
end
|
15
|
+
Replay::Inflector.extend(Replay::Inflector::Proof)
|
16
|
+
|
17
|
+
title "Replay::Inflector"
|
18
|
+
|
19
|
+
proof "camelize camel-cases a string" do
|
20
|
+
#Replay::Inflector.prove{ camelizes?("replay.test", "Replay::Test")}
|
21
|
+
end
|
22
|
+
|
23
|
+
proof "underscore transforms a classname to 'module.class'" do
|
24
|
+
Replay::Inflector.prove{ underscores?("Replay::Test", 'replay.test')}
|
25
|
+
Replay::Inflector.prove{ underscores?("ReplayTest::Test", 'replay_test.test')}
|
26
|
+
Replay::Inflector.prove{ underscores?("Replay2Test::Test", 'replay2_test.test')}
|
27
|
+
Replay::Inflector.prove{ underscores?("Replay::TestData", 'replay.test_data')}
|
28
|
+
end
|
29
|
+
|
30
|
+
proof "constantize finds the constant for a given string, if defined" do
|
31
|
+
Replay::Inflector.prove{ constantizes? "Replay::ReplayError", Replay::ReplayError }
|
32
|
+
end
|