replay 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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