replay 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +10 -0
- data/README +27 -0
- data/Rakefile +1 -0
- data/lib/replay.rb +14 -0
- data/lib/replay/active_record_event_store.rb +32 -0
- data/lib/replay/configuration.rb +10 -0
- data/lib/replay/domain.rb +33 -0
- data/lib/replay/event.rb +27 -0
- data/lib/replay/event_store.rb +55 -0
- data/lib/replay/projector.rb +19 -0
- data/lib/replay/test_storage.rb +8 -0
- data/lib/replay/unknown_event_error.rb +2 -0
- data/lib/replay/version.rb +3 -0
- data/replay.gemspec +26 -0
- data/test/spec_helper.rb +6 -0
- data/test/test_events.sqlite3 +0 -0
- data/test/unit/active_record_event_store_spec.rb +24 -0
- data/test/unit/domain_spec.rb +53 -0
- data/test/unit/event_spec.rb +13 -0
- data/test/unit/event_store_spec.rb +28 -0
- data/test/unit/projector_spec.rb +19 -0
- metadata +123 -0
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.2@replay
|
data/Gemfile
ADDED
data/README
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class Domain::Model
|
2
|
+
include Replay::Domain
|
3
|
+
|
4
|
+
def twitter_update
|
5
|
+
#do stuff
|
6
|
+
signal_event "twitter_updated", status
|
7
|
+
self.save
|
8
|
+
end
|
9
|
+
|
10
|
+
apply "twitter_updated" do |status|
|
11
|
+
model.twitter_status = status
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
#usage
|
18
|
+
model = Model.find(params[:model_id])
|
19
|
+
model.twitter_update
|
20
|
+
|
21
|
+
#listener
|
22
|
+
class TweetReport
|
23
|
+
include Replay::Projector
|
24
|
+
listen :twitter_updated do |model_id, status|
|
25
|
+
#update read model/report/whatever...
|
26
|
+
end
|
27
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/replay.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "replay/version"
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'replay/unknown_event_error'
|
4
|
+
require 'replay/test_storage'
|
5
|
+
require 'replay/configuration'
|
6
|
+
require 'replay/event'
|
7
|
+
require 'replay/event_store'
|
8
|
+
require 'replay/active_record_event_store'
|
9
|
+
require 'replay/domain'
|
10
|
+
require 'replay/projector'
|
11
|
+
|
12
|
+
module Replay
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
if defined?(ActiveRecord)
|
2
|
+
module Replay
|
3
|
+
class ActiveRecordEvent < ::ActiveRecord::Base
|
4
|
+
set_table_name "replay_events"
|
5
|
+
serialize :arguments
|
6
|
+
end
|
7
|
+
|
8
|
+
#needs model id
|
9
|
+
class ActiveRecordEventLogEntry < ::ActiveRecord::Base
|
10
|
+
set_table_name "replay_event_log_entries"
|
11
|
+
serialize :data
|
12
|
+
end
|
13
|
+
|
14
|
+
class ActiveRecordEventStore
|
15
|
+
def store(event, model_id, *args)
|
16
|
+
ar_event = Replay::ActiveRecordEvent.new
|
17
|
+
ar_event.event = event
|
18
|
+
ar_event.model_id = model_id
|
19
|
+
ar_event.arguments = args
|
20
|
+
ar_event.save
|
21
|
+
end
|
22
|
+
|
23
|
+
def log_event(event, model_id, *args)
|
24
|
+
log_entry = Replay::ActiveRecordEventLogEntry.new
|
25
|
+
log_entry.event = event
|
26
|
+
log_entry.model_id = model_id
|
27
|
+
log_entry.data = args
|
28
|
+
log_entry.save
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Replay::Domain
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
include Replay
|
4
|
+
|
5
|
+
included do
|
6
|
+
self.extend ClassMethods
|
7
|
+
self.event_blocks = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def signal_event(event, *args)
|
11
|
+
EventStore.handle_event(event, self.id, *args)
|
12
|
+
apply_event(event, *args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def apply_event(event, *args)
|
16
|
+
event_block = event_blocks[event.to_sym]
|
17
|
+
raise UnknownEventError.new("#{event} is not a known event on class #{self.class.name}") unless event_block
|
18
|
+
EventStore.log_event(event, self.id, *args)
|
19
|
+
self.instance_exec(*args, &event_block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def event_blocks
|
23
|
+
self.class.event_blocks
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def apply(event, &block)
|
28
|
+
event_blocks[event.to_sym] = block
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_accessor :event_blocks
|
32
|
+
end
|
33
|
+
end
|
data/lib/replay/event.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Replay
|
2
|
+
class Event
|
3
|
+
attr_accessor :event_type, :attributes
|
4
|
+
|
5
|
+
def initialize(event_type, *data)
|
6
|
+
@event_type = event_type
|
7
|
+
if data.first.kind_of? Hash
|
8
|
+
@attributes = HashWithIndifferentAccess.new data.first.dup
|
9
|
+
else
|
10
|
+
@attributes = HashWithIndifferentAccess.new({data.first => data.last})
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method, *args)
|
15
|
+
method_root = method.to_s.gsub(/=$/, "")
|
16
|
+
if @attributes && @attributes.has_key?(method_root)
|
17
|
+
if method.to_s[/=$/]
|
18
|
+
@attributes[method_root] = args.first
|
19
|
+
else
|
20
|
+
@attributes[method]
|
21
|
+
end
|
22
|
+
else
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Replay
|
2
|
+
class EventStore
|
3
|
+
include Replay
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :configuration
|
7
|
+
attr_accessor :listeners
|
8
|
+
attr_accessor :test_mode
|
9
|
+
attr_accessor :event_stream
|
10
|
+
end
|
11
|
+
self.listeners = {}
|
12
|
+
self.configuration = Replay::Configuration.new
|
13
|
+
self.event_stream = []
|
14
|
+
|
15
|
+
def self.configure(&block)
|
16
|
+
block.call(self.configuration)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.storage
|
20
|
+
configuration.storage
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.add_listener(event, new_listener)
|
24
|
+
self.listeners[event] = [] unless self.listeners[event]
|
25
|
+
self.listeners[event] << new_listener
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.clear_listeners
|
29
|
+
listeners = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.log_event(event, model_id, *args)
|
33
|
+
if configuration.storage
|
34
|
+
configuration.storage.each do |event_store_adapter|
|
35
|
+
event_store_adapter.log_event(event, model_id, *args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.handle_event(event, model_id, *args)
|
41
|
+
self.event_stream << Event.new(event, args.last) if self.test_mode
|
42
|
+
|
43
|
+
if configuration.storage
|
44
|
+
configuration.storage.each do |event_store_adapter|
|
45
|
+
event_store_adapter.store(event, model_id, *args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
if self.listeners[event]
|
49
|
+
self.listeners[event].each do |listener|
|
50
|
+
listener.handle_event(event, model_id, *args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Replay::Projector
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
self.extend(ClassMethods)
|
6
|
+
self.listening_blocks = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def listen(event, &block)
|
11
|
+
listening_blocks[event] = [] unless listening_blocks[event]
|
12
|
+
listening_blocks[event] << block
|
13
|
+
Replay::EventStore.add_listener(event, self)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
attr_accessor :listening_blocks
|
18
|
+
end
|
19
|
+
end
|
data/replay.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "replay/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "replay"
|
7
|
+
s.version = Replay::VERSION
|
8
|
+
s.authors = ["karmajunkie"]
|
9
|
+
s.email = ["keith.gaddis@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Replay supports event-sourced data models.}
|
12
|
+
s.description = %q{Replay supports event-sourced data models.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "replay"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "activerecord"
|
23
|
+
s.add_development_dependency "sqlite3-ruby"
|
24
|
+
# s.add_runtime_dependency "rest-client"
|
25
|
+
s.add_runtime_dependency "activesupport"
|
26
|
+
end
|
data/test/spec_helper.rb
ADDED
Binary file
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require './test/spec_helper'
|
2
|
+
|
3
|
+
::ActiveRecord::Base.establish_connection(
|
4
|
+
:adapter => "sqlite3",
|
5
|
+
:database => "test/test_events.sqlite3"
|
6
|
+
)
|
7
|
+
::ActiveRecord::Base.connection.execute("create table if not exists replay_events(id primary key, event, model_id, arguments, created_at, updated_at)")
|
8
|
+
|
9
|
+
describe Replay::ActiveRecordEventStore do
|
10
|
+
before do
|
11
|
+
@store = Replay::ActiveRecordEventStore.new
|
12
|
+
end
|
13
|
+
def action
|
14
|
+
@store.store(:foo_event, 123, 'bar')
|
15
|
+
end
|
16
|
+
describe "#store" do
|
17
|
+
it "should create an event in the database" do
|
18
|
+
count = Replay::ActiveRecordEvent.count
|
19
|
+
action
|
20
|
+
(Replay::ActiveRecordEvent.count - count).must_equal 1
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require './test/spec_helper'
|
2
|
+
|
3
|
+
describe Replay do
|
4
|
+
class ReplayTestClass
|
5
|
+
include Replay::Domain
|
6
|
+
|
7
|
+
attr_accessor :bar_value
|
8
|
+
|
9
|
+
def id
|
10
|
+
1
|
11
|
+
end
|
12
|
+
|
13
|
+
def foo
|
14
|
+
signal_event :foo_happened, "bar"
|
15
|
+
end
|
16
|
+
|
17
|
+
def bad_foo
|
18
|
+
signal_event :no_foo_lovey
|
19
|
+
end
|
20
|
+
|
21
|
+
apply :foo_happened do |what|
|
22
|
+
self.bar_value = what
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
before do
|
27
|
+
@something = ReplayTestClass.new
|
28
|
+
end
|
29
|
+
describe "behavior with signalling" do
|
30
|
+
it "raises an error for unknown events" do
|
31
|
+
lambda{@something.bad_foo}.must_raise UnknownEventError
|
32
|
+
end
|
33
|
+
|
34
|
+
it "sends the event to the EventStore" do
|
35
|
+
Replay::EventStore.expects(:handle_event).with(:foo_happened, 1, 'bar')
|
36
|
+
@something.foo
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "rebuilding" do
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "applying an event" do
|
45
|
+
it "changes state" do
|
46
|
+
@something.foo
|
47
|
+
@something.bar_value.must_equal "bar"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require './test/spec_helper'
|
2
|
+
|
3
|
+
describe Replay::EventStore do
|
4
|
+
before do
|
5
|
+
Replay::EventStore.configure do |config|
|
6
|
+
config.storage = Replay::TestStorage.new
|
7
|
+
end
|
8
|
+
end
|
9
|
+
after do
|
10
|
+
Replay::EventStore.clear_listeners
|
11
|
+
end
|
12
|
+
describe "#handle_event" do
|
13
|
+
def action
|
14
|
+
Replay::EventStore.handle_event(:foo_event, 123, {:bar => "fly"})
|
15
|
+
end
|
16
|
+
it "stores the event" do
|
17
|
+
Replay::TestStorage.any_instance.expects(:store).with(:foo_event, 123, {:bar => "fly"} )
|
18
|
+
action
|
19
|
+
end
|
20
|
+
it "notifies listeners" do
|
21
|
+
listener = mock("listener") do
|
22
|
+
expects(:handle_event).with(:foo_event, 123, {:bar => 'fly'})
|
23
|
+
end
|
24
|
+
Replay::EventStore.add_listener(:foo_event, listener)
|
25
|
+
action
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require './test/spec_helper'
|
2
|
+
|
3
|
+
describe Replay::Projector do
|
4
|
+
after do
|
5
|
+
Replay::EventStore.clear_listeners
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#listen" do
|
9
|
+
it "adds the class as a listener to the EventStore" do
|
10
|
+
class ProjectorExample
|
11
|
+
Replay::EventStore.expects(:add_listener).with(:foo_happened, self)
|
12
|
+
|
13
|
+
include Replay::Projector
|
14
|
+
listen :foo_happened do |model_id, arg|
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: replay
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- karmajunkie
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sqlite3-ruby
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: activesupport
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Replay supports event-sourced data models.
|
63
|
+
email:
|
64
|
+
- keith.gaddis@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rvmrc
|
71
|
+
- Gemfile
|
72
|
+
- README
|
73
|
+
- Rakefile
|
74
|
+
- lib/replay.rb
|
75
|
+
- lib/replay/active_record_event_store.rb
|
76
|
+
- lib/replay/configuration.rb
|
77
|
+
- lib/replay/domain.rb
|
78
|
+
- lib/replay/event.rb
|
79
|
+
- lib/replay/event_store.rb
|
80
|
+
- lib/replay/projector.rb
|
81
|
+
- lib/replay/test_storage.rb
|
82
|
+
- lib/replay/unknown_event_error.rb
|
83
|
+
- lib/replay/version.rb
|
84
|
+
- replay.gemspec
|
85
|
+
- test/spec_helper.rb
|
86
|
+
- test/test_events.sqlite3
|
87
|
+
- test/unit/active_record_event_store_spec.rb
|
88
|
+
- test/unit/domain_spec.rb
|
89
|
+
- test/unit/event_spec.rb
|
90
|
+
- test/unit/event_store_spec.rb
|
91
|
+
- test/unit/projector_spec.rb
|
92
|
+
homepage: ''
|
93
|
+
licenses: []
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project: replay
|
112
|
+
rubygems_version: 1.8.24
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Replay supports event-sourced data models.
|
116
|
+
test_files:
|
117
|
+
- test/spec_helper.rb
|
118
|
+
- test/test_events.sqlite3
|
119
|
+
- test/unit/active_record_event_store_spec.rb
|
120
|
+
- test/unit/domain_spec.rb
|
121
|
+
- test/unit/event_spec.rb
|
122
|
+
- test/unit/event_store_spec.rb
|
123
|
+
- test/unit/projector_spec.rb
|