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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.rvmrc +1 -1
  3. data/Gemfile +7 -3
  4. data/Guardfile +10 -0
  5. data/LICENSE +21 -0
  6. data/README.md +153 -0
  7. data/Rakefile +15 -0
  8. data/lib/replay.rb +39 -10
  9. data/lib/replay/backends.rb +50 -0
  10. data/lib/replay/event_declarations.rb +36 -0
  11. data/lib/replay/event_decorator.rb +13 -0
  12. data/lib/replay/events.rb +24 -0
  13. data/lib/replay/inflector.rb +55 -0
  14. data/lib/replay/observer.rb +18 -0
  15. data/lib/replay/publisher.rb +72 -0
  16. data/lib/replay/repository.rb +61 -0
  17. data/lib/replay/repository/configuration.rb +30 -0
  18. data/lib/replay/repository/identity_map.rb +25 -0
  19. data/lib/replay/router.rb +5 -0
  20. data/lib/replay/router/default_router.rb +21 -0
  21. data/lib/replay/rspec.rb +50 -0
  22. data/lib/replay/subscription_manager.rb +28 -0
  23. data/lib/replay/test.rb +64 -0
  24. data/lib/replay/test/test_event_stream.rb +19 -0
  25. data/lib/replay/version.rb +1 -1
  26. data/proofs/all.rb +7 -0
  27. data/proofs/proofs_init.rb +10 -0
  28. data/proofs/replay/inflector_proof.rb +32 -0
  29. data/proofs/replay/publisher_proof.rb +170 -0
  30. data/proofs/replay/repository_configuration_proof.rb +67 -0
  31. data/proofs/replay/repository_proof.rb +46 -0
  32. data/proofs/replay/subscriber_manager_proof.rb +39 -0
  33. data/proofs/replay/test_proof.rb +28 -0
  34. data/replay.gemspec +5 -4
  35. data/test/replay/observer_spec.rb +37 -0
  36. data/test/replay/router/default_router_spec.rb +43 -0
  37. data/test/test_helper.rb +10 -0
  38. metadata +65 -48
  39. data/README +0 -27
  40. data/lib/replay/active_record_event_store.rb +0 -32
  41. data/lib/replay/domain.rb +0 -33
  42. data/lib/replay/event.rb +0 -27
  43. data/lib/replay/event_store.rb +0 -55
  44. data/lib/replay/projector.rb +0 -19
  45. data/lib/replay/test_storage.rb +0 -8
  46. data/lib/replay/unknown_event_error.rb +0 -2
  47. data/test/spec_helper.rb +0 -6
  48. data/test/test_events.sqlite3 +0 -0
  49. data/test/unit/active_record_event_store_spec.rb +0 -24
  50. data/test/unit/domain_spec.rb +0 -53
  51. data/test/unit/event_spec.rb +0 -13
  52. data/test/unit/event_store_spec.rb +0 -28
  53. 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,5 @@
1
+ module Replay
2
+ module Router
3
+
4
+ end
5
+ 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)
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Replay
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/proofs/all.rb ADDED
@@ -0,0 +1,7 @@
1
+ require_relative 'proofs_init.rb'
2
+
3
+ files = Dir.glob(File.join(File.dirname(__FILE__), '**/*_proof.rb'))
4
+ puts files
5
+ Proof::Suite.run "replay/**/*.rb"
6
+
7
+
@@ -0,0 +1,10 @@
1
+ $:<<File.expand_path("./lib")
2
+ $:<<File.expand_path("./proofs")
3
+
4
+ require 'replay'
5
+
6
+ require 'proof'
7
+
8
+ include Proof
9
+
10
+
@@ -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