replay 0.0.1
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.
- 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
|