replay 0.0.1 → 0.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 +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
|