eventus 0.2.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.
- data/.gitignore +6 -0
- data/Gemfile +11 -0
- data/Guardfile +6 -0
- data/README.md +36 -0
- data/Rakefile +4 -0
- data/eventus.gemspec +20 -0
- data/lib/eventus/aggregate_root.rb +48 -0
- data/lib/eventus/errors.rb +3 -0
- data/lib/eventus/persistence/in_memory.rb +42 -0
- data/lib/eventus/persistence/kyotocabinet.rb +45 -0
- data/lib/eventus/persistence.rb +6 -0
- data/lib/eventus/serializers/marshal.rb +15 -0
- data/lib/eventus/serializers/msgpack.rb +17 -0
- data/lib/eventus/serializers.rb +6 -0
- data/lib/eventus/store.rb +12 -0
- data/lib/eventus/stream.rb +39 -0
- data/lib/eventus/version.rb +3 -0
- data/lib/eventus.rb +17 -0
- data/spec/aggregate_root_spec.rb +69 -0
- data/spec/persistence/in_memory_spec.rb +84 -0
- data/spec/persistence/kyotocabinet_spec.rb +98 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/store_spec.rb +22 -0
- data/spec/stream_spec.rb +76 -0
- metadata +81 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
eventus
|
2
|
+
===
|
3
|
+
|
4
|
+
A Ruby Event Store
|
5
|
+
|
6
|
+
License
|
7
|
+
---
|
8
|
+
|
9
|
+
Copyright (c) 2012, Jason Staten, PeerIntel
|
10
|
+
|
11
|
+
All rights reserved.
|
12
|
+
|
13
|
+
Redistribution and use in source and binary forms, with or without
|
14
|
+
modification, are permitted provided that the following conditions are met:
|
15
|
+
|
16
|
+
- Redistributions of source code must retain the above copyright notice, this
|
17
|
+
list of conditions and the following disclaimer.
|
18
|
+
|
19
|
+
- Redistributions in binary form must reproduce the above copyright notice,
|
20
|
+
this list of conditions and the following disclaimer in the documentation
|
21
|
+
and/or other materials provided with the distribution.
|
22
|
+
|
23
|
+
- Neither the name of PeerIntel nor the names of its contributors may be used
|
24
|
+
to endorse or promote products derived from this software without specific
|
25
|
+
prior written permission.
|
26
|
+
|
27
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
28
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
29
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
30
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
31
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
32
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
33
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
34
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
35
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
36
|
+
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/Rakefile
ADDED
data/eventus.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "eventus/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "eventus"
|
7
|
+
s.version = Eventus::VERSION
|
8
|
+
s.authors = ["Jason Staten"]
|
9
|
+
s.email = ["jstaten07@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Event Store}
|
12
|
+
s.description = %q{An Event Store}
|
13
|
+
|
14
|
+
s.rubyforge_project = "eventus"
|
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
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Eventus
|
2
|
+
module AggregateRoot
|
3
|
+
module ClassMethods
|
4
|
+
def find(id)
|
5
|
+
instance = self.new
|
6
|
+
stream = Eventus::Stream.new(id, persistence)
|
7
|
+
instance.populate(stream)
|
8
|
+
instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply(event_name, &block)
|
12
|
+
raise "A block is required" unless block_given?
|
13
|
+
define_method("apply_#{event_name}", &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def persistence
|
17
|
+
@persistence ||= Eventus.persistence
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module InstanceMethods
|
22
|
+
def populate(stream)
|
23
|
+
@stream = stream
|
24
|
+
stream.committed_events.each do |event|
|
25
|
+
apply_change event[:name], event[:body], false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def save
|
30
|
+
@stream.commit
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def apply_change(name, body=nil, is_new=true)
|
36
|
+
method_name = "apply_#{name}"
|
37
|
+
self.send method_name, body if self.respond_to?(method_name)
|
38
|
+
|
39
|
+
@stream.add({:name => name, :body => body}) if is_new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.included(base)
|
44
|
+
base.send :include, InstanceMethods
|
45
|
+
base.send :extend, ClassMethods
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Eventus
|
2
|
+
module Persistence
|
3
|
+
class InMemory
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@store = {}
|
7
|
+
@serializer = options.fetch(:serializer) { Eventus::Serializers::Marshal }
|
8
|
+
@mutex = Mutex.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def commit(id, start, events)
|
12
|
+
@mutex.synchronize do
|
13
|
+
pending = {}
|
14
|
+
events.each_with_index do |event, index|
|
15
|
+
key = build_key(id, start + index)
|
16
|
+
raise Eventus::ConcurrencyError if @store.include? key
|
17
|
+
value = @serializer.serialize(event)
|
18
|
+
pending[key] = value
|
19
|
+
end
|
20
|
+
@store.merge! pending
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def load(id, min=nil)
|
25
|
+
@mutex.synchronize do
|
26
|
+
keys = @store.keys.select { |k| k.start_with? id }.sort
|
27
|
+
|
28
|
+
if min
|
29
|
+
min_key = build_key(id, min)
|
30
|
+
keys = keys.drop_while { |k| k != min_key }
|
31
|
+
end
|
32
|
+
|
33
|
+
keys.map { |k| @serializer.deserialize(@store[k]) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_key(id, index)
|
38
|
+
id + ("_%07d" % index)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'kyotocabinet'
|
2
|
+
|
3
|
+
module Eventus
|
4
|
+
module Persistence
|
5
|
+
class KyotoCabinet
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@db = ::KyotoCabinet::DB::new
|
9
|
+
@db.open(options[:path], ::KyotoCabinet::DB::OCREATE)
|
10
|
+
@serializer = options.fetch(:serializer) { Eventus::Serializers::Marshal }
|
11
|
+
end
|
12
|
+
|
13
|
+
def commit(id, start, events)
|
14
|
+
pid = pack_hex(id)
|
15
|
+
@db.transaction do
|
16
|
+
events.each_with_index do |event, index|
|
17
|
+
key = build_key(pid, start + index)
|
18
|
+
value = @serializer.serialize(event)
|
19
|
+
raise Eventus::ConcurrencyError unless @db.add(key,value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def load(id, min = nil)
|
25
|
+
pid = pack_hex(id)
|
26
|
+
keys = @db.match_prefix(pid)
|
27
|
+
|
28
|
+
if min
|
29
|
+
min_key = build_key(pid, min)
|
30
|
+
keys = keys.drop_while { |k| k != min_key }
|
31
|
+
end
|
32
|
+
|
33
|
+
@db.get_bulk(keys, false).values.map { |obj| @serializer.deserialize(obj) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def pack_hex(id)
|
37
|
+
id.match(/^[0-9a-fA-F]+$/) ? [id].pack('H*') : id
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_key(id, index)
|
41
|
+
id + ("_%07d" % index)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Eventus
|
2
|
+
class Stream
|
3
|
+
|
4
|
+
attr_reader :id, :committed_events, :uncommitted_events
|
5
|
+
|
6
|
+
def initialize(id, persistence)
|
7
|
+
@id = id
|
8
|
+
@persistence = persistence
|
9
|
+
@committed_events = []
|
10
|
+
@uncommitted_events = []
|
11
|
+
load_events @persistence.load(id)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(event)
|
15
|
+
@uncommitted_events << event
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :<<, :add
|
19
|
+
|
20
|
+
def commit
|
21
|
+
@persistence.commit @id, version, @uncommitted_events
|
22
|
+
load_events @uncommitted_events
|
23
|
+
@uncommitted_events.clear
|
24
|
+
rescue ConcurrencyError => e
|
25
|
+
load_events @persistence.load(id, version + 1)
|
26
|
+
raise e
|
27
|
+
end
|
28
|
+
|
29
|
+
def version
|
30
|
+
@committed_events.length
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def load_events(events)
|
36
|
+
events.each { |e| @committed_events << e }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/eventus.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Eventus
|
2
|
+
autoload :Serializers, 'eventus/serializers'
|
3
|
+
autoload :AggregateRoot, 'eventus/aggregate_root'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def persistence
|
8
|
+
@persistence ||= Eventus::Persistence::InMemory.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def persistence=(val)
|
12
|
+
@persistence = val
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
%w{store stream version persistence errors}.each { |r| require "eventus/#{r}" }
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestAgg
|
4
|
+
include Eventus::AggregateRoot
|
5
|
+
|
6
|
+
attr_accessor :loaded
|
7
|
+
|
8
|
+
def bake_cake
|
9
|
+
apply_change :cake_baked, :flavor => 'strawberry'
|
10
|
+
end
|
11
|
+
|
12
|
+
apply :dino do |e|
|
13
|
+
@loaded = true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe Eventus::AggregateRoot do
|
18
|
+
let(:events) { [] }
|
19
|
+
let(:persistence) { stub.as_null_object }
|
20
|
+
|
21
|
+
before do
|
22
|
+
TestAgg.stub(:persistence).and_return(persistence)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should load a new aggregate from find" do
|
26
|
+
result = TestAgg.find('abc')
|
27
|
+
result.should_not be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "when events exist" do
|
31
|
+
before do
|
32
|
+
persistence.should_receive(:load).with('abc').and_return(events)
|
33
|
+
events << {:name => 'dino', :body => {}}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should apply the event" do
|
37
|
+
result = TestAgg.find('abc')
|
38
|
+
result.loaded.should == true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "when applying a new change" do
|
43
|
+
let(:aggregate) { TestAgg.new }
|
44
|
+
let(:stream) { stub(:stream, :committed_events => []) }
|
45
|
+
|
46
|
+
before do
|
47
|
+
aggregate.populate(stream)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should be added to the stream" do
|
51
|
+
stream.should_receive(:add)
|
52
|
+
aggregate.bake_cake
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "when saving" do
|
57
|
+
let(:aggregate) { TestAgg.new }
|
58
|
+
let(:stream) { stub(:stream, :committed_events => []) }
|
59
|
+
|
60
|
+
before do
|
61
|
+
aggregate.populate(stream)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should commit stream" do
|
65
|
+
stream.should_receive(:commit)
|
66
|
+
aggregate.save
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventus::Persistence::InMemory do
|
4
|
+
let(:options) { {:path => '%'} }
|
5
|
+
let(:persistence) { Eventus::Persistence::InMemory.new(options) }
|
6
|
+
let(:uuid) { UUID.new }
|
7
|
+
|
8
|
+
it "should store complex objects" do
|
9
|
+
id = uuid.generate :compact
|
10
|
+
o = {'a' => 'super', 'complex' => ['object', 'with', {'nested' => ['members', 'galore', 1]}]}
|
11
|
+
persistence.commit id, 1, [o]
|
12
|
+
|
13
|
+
result = persistence.load id
|
14
|
+
result[0].should == o
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return no events when key not found" do
|
18
|
+
result = persistence.load "my_id"
|
19
|
+
result.should be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should return events ordered" do
|
23
|
+
id = uuid.generate :compact
|
24
|
+
persistence.commit id, 5, ["five", "six"]
|
25
|
+
persistence.commit id, 1, ["one", "two"]
|
26
|
+
persistence.commit id, 3, ["three", "four"]
|
27
|
+
persistence.commit "other", 1, ["cake", "batter"]
|
28
|
+
|
29
|
+
result = persistence.load id
|
30
|
+
result.should == ["one", "two", "three", "four", "five", "six"]
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "when events exist" do
|
34
|
+
let(:id) { uuid.generate :compact }
|
35
|
+
let(:events) { (1..20).map {|i| "Body #{i}"} }
|
36
|
+
before do
|
37
|
+
persistence.commit id, 1, events
|
38
|
+
other_events = (1..60).map {|i| "Other #{i}"}
|
39
|
+
persistence.commit uuid.generate(:compact), 1, events
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should load events" do
|
43
|
+
result = persistence.load id
|
44
|
+
result.length.should == 20
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should throw concurrency exception if the same event number is added" do
|
48
|
+
lambda {persistence.commit id, 3, ["This is taken"]}.should raise_error(Eventus::ConcurrencyError)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should rollback changes on concurrency error" do
|
52
|
+
persistence.commit id, 3, ["first", "second", "third"] rescue nil
|
53
|
+
|
54
|
+
result = persistence.load id
|
55
|
+
result.length.should == 20
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should load all events from a minimum" do
|
59
|
+
result = persistence.load id, 10
|
60
|
+
result.length.should == 11
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "when serialization is set" do
|
65
|
+
let(:serializer) { stub }
|
66
|
+
before do
|
67
|
+
options[:serializer] = serializer
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should use serializer" do
|
71
|
+
input = "original"
|
72
|
+
ser = "i'm serialized!"
|
73
|
+
|
74
|
+
serializer.should_receive(:serialize).with(input).and_return(ser)
|
75
|
+
serializer.should_receive(:deserialize).with(ser).and_return(input)
|
76
|
+
|
77
|
+
id = uuid.generate :compact
|
78
|
+
|
79
|
+
persistence.commit id, 1, [input]
|
80
|
+
result = persistence.load id
|
81
|
+
result[0].should == input
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventus::Persistence::KyotoCabinet do
|
4
|
+
let(:options) { {:path => '%'} }
|
5
|
+
let(:persistence) { Eventus::Persistence::KyotoCabinet.new(options) }
|
6
|
+
let(:uuid) { UUID.new }
|
7
|
+
|
8
|
+
it "should pack keys" do
|
9
|
+
1000.times do
|
10
|
+
key = uuid.generate(:compact)
|
11
|
+
packed = persistence.pack_hex(key)
|
12
|
+
packed.unpack('H*')[0].should == key
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should not pack non-hex strings" do
|
17
|
+
key = 'abc123q'
|
18
|
+
packed = persistence.pack_hex(key)
|
19
|
+
packed.should == key
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should store complex objects" do
|
23
|
+
id = uuid.generate :compact
|
24
|
+
o = {'a' => 'super', 'complex' => ['object', 'with', {'nested' => ['members', 'galore', 1]}]}
|
25
|
+
persistence.commit id, 1, [o]
|
26
|
+
|
27
|
+
result = persistence.load id
|
28
|
+
result[0].should == o
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return no events when key not found" do
|
32
|
+
result = persistence.load "my_id"
|
33
|
+
result.should be_empty
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should return events ordered" do
|
37
|
+
id = uuid.generate :compact
|
38
|
+
persistence.commit id, 5, ["five", "six"]
|
39
|
+
persistence.commit id, 1, ["one", "two"]
|
40
|
+
persistence.commit id, 3, ["three", "four"]
|
41
|
+
persistence.commit "other", 1, ["cake", "batter"]
|
42
|
+
|
43
|
+
result = persistence.load id
|
44
|
+
result.should == ["one", "two", "three", "four", "five", "six"]
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "when events exist" do
|
48
|
+
let(:id) { uuid.generate :compact }
|
49
|
+
let(:events) { (1..20).map {|i| "Body #{i}"} }
|
50
|
+
before do
|
51
|
+
persistence.commit id, 1, events
|
52
|
+
other_events = (1..60).map {|i| "Other #{i}"}
|
53
|
+
persistence.commit uuid.generate(:compact), 1, events
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should load events" do
|
57
|
+
result = persistence.load id
|
58
|
+
result.length.should == 20
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should throw concurrency exception if the same event number is added" do
|
62
|
+
lambda {persistence.commit id, 3, ["This is taken"]}.should raise_error(Eventus::ConcurrencyError)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should rollback changes on concurrency error" do
|
66
|
+
persistence.commit id, 3, ["first", "second", "third"] rescue nil
|
67
|
+
|
68
|
+
result = persistence.load id
|
69
|
+
result.length.should == 20
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should load all events from a minimum" do
|
73
|
+
result = persistence.load id, 10
|
74
|
+
result.length.should == 11
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "when serialization is set" do
|
79
|
+
let(:serializer) { stub }
|
80
|
+
before do
|
81
|
+
options[:serializer] = serializer
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should use serializer" do
|
85
|
+
input = "original"
|
86
|
+
ser = "i'm serialized!"
|
87
|
+
|
88
|
+
serializer.should_receive(:serialize).with(input).and_return(ser)
|
89
|
+
serializer.should_receive(:deserialize).with(ser).and_return(input)
|
90
|
+
|
91
|
+
id = uuid.generate :compact
|
92
|
+
|
93
|
+
persistence.commit id, 1, [input]
|
94
|
+
result = persistence.load id
|
95
|
+
result[0].should == input
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
5
|
+
require 'eventus'
|
6
|
+
|
7
|
+
Bundler.require :development
|
8
|
+
|
9
|
+
Dir[File.join(File.dirname(__FILE__), 'support', '*.rb')].each { |d| require d }
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.mock_with :rspec
|
13
|
+
end
|
data/spec/store_spec.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventus::Store do
|
4
|
+
let(:persistence) { stub(:persistence).as_null_object }
|
5
|
+
let(:store) { Eventus::Store.new(persistence) }
|
6
|
+
let(:uuid) { UUID.new }
|
7
|
+
let(:stream) { stub(:stream) }
|
8
|
+
|
9
|
+
describe "when opening an event stream" do
|
10
|
+
let(:id) { uuid.generate(:compact) }
|
11
|
+
let(:result) { store.open id }
|
12
|
+
|
13
|
+
it "should request from persistence" do
|
14
|
+
persistence.should_receive(:get_events)
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should return a stream" do
|
19
|
+
result.should_not be_nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/spec/stream_spec.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventus::Stream do
|
4
|
+
let(:id) { UUID.generate(:compact) }
|
5
|
+
let(:stream) { Eventus::Stream.new(id, persistence) }
|
6
|
+
let(:persistence) { stub(:persistence).as_null_object }
|
7
|
+
|
8
|
+
it "should use id" do
|
9
|
+
stream.id.should == id
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should have no committed events" do
|
13
|
+
stream.committed_events.should be_empty
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have no uncommitted events" do
|
17
|
+
stream.uncommitted_events.should be_empty
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "when events available from persistence" do
|
21
|
+
before do
|
22
|
+
persistence.should_receive(:load).and_return([stub, stub])
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should have an equal number of events" do
|
26
|
+
stream.version.should == 2
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should have no uncommitted events" do
|
30
|
+
stream.uncommitted_events.should be_empty
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "when events added" do
|
35
|
+
before do
|
36
|
+
stream << stub
|
37
|
+
stream.add stub
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have uncommitted events" do
|
41
|
+
stream.uncommitted_events.length.should == 2
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "when committed" do
|
45
|
+
before do
|
46
|
+
persistence.should_receive(:commit)
|
47
|
+
stream.commit
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should have committed events" do
|
51
|
+
stream.version.should == 2
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should have no uncommitted events" do
|
55
|
+
stream.uncommitted_events.should be_empty
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "when a concurrency error occurs" do
|
61
|
+
before do
|
62
|
+
persistence.should_receive(:commit).and_raise(Eventus::ConcurrencyError)
|
63
|
+
stream << stub(:event)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should reraise concurrency error" do
|
67
|
+
lambda {stream.commit}.should raise_error(Eventus::ConcurrencyError)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should load latest events" do
|
71
|
+
persistence.should_receive(:load).with(id, 1).and_return([stub, stub, stub])
|
72
|
+
stream.commit rescue nil
|
73
|
+
stream.version.should == 3
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: eventus
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jason Staten
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-09 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: An Event Store
|
15
|
+
email:
|
16
|
+
- jstaten07@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- Guardfile
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- eventus.gemspec
|
27
|
+
- lib/eventus.rb
|
28
|
+
- lib/eventus/aggregate_root.rb
|
29
|
+
- lib/eventus/errors.rb
|
30
|
+
- lib/eventus/persistence.rb
|
31
|
+
- lib/eventus/persistence/in_memory.rb
|
32
|
+
- lib/eventus/persistence/kyotocabinet.rb
|
33
|
+
- lib/eventus/serializers.rb
|
34
|
+
- lib/eventus/serializers/marshal.rb
|
35
|
+
- lib/eventus/serializers/msgpack.rb
|
36
|
+
- lib/eventus/store.rb
|
37
|
+
- lib/eventus/stream.rb
|
38
|
+
- lib/eventus/version.rb
|
39
|
+
- spec/aggregate_root_spec.rb
|
40
|
+
- spec/persistence/in_memory_spec.rb
|
41
|
+
- spec/persistence/kyotocabinet_spec.rb
|
42
|
+
- spec/spec_helper.rb
|
43
|
+
- spec/store_spec.rb
|
44
|
+
- spec/stream_spec.rb
|
45
|
+
homepage: ''
|
46
|
+
licenses: []
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
hash: 3611850257360750717
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
hash: 3611850257360750717
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project: eventus
|
71
|
+
rubygems_version: 1.8.13
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Event Store
|
75
|
+
test_files:
|
76
|
+
- spec/aggregate_root_spec.rb
|
77
|
+
- spec/persistence/in_memory_spec.rb
|
78
|
+
- spec/persistence/kyotocabinet_spec.rb
|
79
|
+
- spec/spec_helper.rb
|
80
|
+
- spec/store_spec.rb
|
81
|
+
- spec/stream_spec.rb
|