jouba 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/Gemfile +3 -2
- data/Gemfile.lock +12 -4
- data/README.md +113 -0
- data/VERSION +1 -1
- data/jouba.gemspec +20 -13
- data/lib/jouba.rb +20 -46
- data/lib/jouba/aggregate.rb +51 -39
- data/lib/jouba/cache.rb +40 -0
- data/lib/jouba/event.rb +27 -7
- data/lib/jouba/key.rb +11 -0
- data/lib/jouba/store.rb +76 -0
- data/spec/integration/customer_spec.rb +83 -0
- data/spec/lib/jouba/aggregate_spec.rb +88 -47
- data/spec/lib/jouba/cache_spec.rb +0 -0
- data/spec/lib/jouba/event_spec.rb +74 -10
- data/spec/lib/jouba/key_spec.rb +32 -0
- data/spec/lib/jouba_spec.rb +36 -182
- data/spec/spec_helper.rb +0 -7
- metadata +28 -10
- data/README.rdoc +0 -19
- data/lib/jouba/exceptions.rb +0 -3
- data/lib/jouba/stores.rb +0 -27
data/lib/jouba/cache.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Jouba
|
4
|
+
module Cache
|
5
|
+
class Null
|
6
|
+
def fetch(_)
|
7
|
+
yield
|
8
|
+
end
|
9
|
+
|
10
|
+
def refresh(_, _)
|
11
|
+
yield
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Memory
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
attr_reader :store
|
19
|
+
|
20
|
+
def_delegators :store, :set, :get, :persist
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@store = MemoryStore.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch(key)
|
27
|
+
get(key) || yield.tap { |value| store.set(key, value) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def refresh(key, value)
|
31
|
+
store.set(key, value)
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.load(file_path)
|
36
|
+
@store = MemoryStore.load(file_path)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/jouba/event.rb
CHANGED
@@ -1,13 +1,33 @@
|
|
1
1
|
module Jouba
|
2
|
-
class Event < Hashie::
|
3
|
-
|
2
|
+
class Event < Hashie::Trash
|
3
|
+
property :key, required: true
|
4
|
+
property :name, required: true
|
5
|
+
property :data, required: true
|
6
|
+
property :uuid, default: ->(e) { e.send(:raw_uuid).to_s }
|
7
|
+
property :version, default: ->(e) { e.send(:raw_uuid).version }
|
8
|
+
property :timestamp, default: ->(e) { e.send(:raw_uuid).timestamp }
|
4
9
|
|
5
|
-
|
6
|
-
|
7
|
-
|
10
|
+
def self.serialize(event)
|
11
|
+
event.to_h
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.deserialize(serialized_event)
|
15
|
+
new(serialized_event)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.stream(key, params)
|
19
|
+
Jouba.Store.get(key, params).map { |event| Event.deserialize(event) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def track
|
23
|
+
Jouba.Store.set(key, Event.serialize(self))
|
24
|
+
end
|
25
|
+
alias_method :save, :track
|
26
|
+
|
27
|
+
private
|
8
28
|
|
9
|
-
def
|
10
|
-
|
29
|
+
def raw_uuid
|
30
|
+
@raw_uuid ||= self[:uuid].nil? ? UUID.new : UUID.new(self[:uuid])
|
11
31
|
end
|
12
32
|
end
|
13
33
|
end
|
data/lib/jouba/key.rb
ADDED
data/lib/jouba/store.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Jouba
|
5
|
+
class MemoryStore
|
6
|
+
attr_reader :db
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
flush
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(key, _ = {})
|
13
|
+
db[key].nil? ? nil : deserialize(db[key])
|
14
|
+
end
|
15
|
+
|
16
|
+
def set(key, value)
|
17
|
+
db[key] = serialize(value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete(key)
|
21
|
+
db.delete(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
def flush
|
25
|
+
@db = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def persist(file_path)
|
29
|
+
File.open(file_path, 'w') { |file| file.write @db.to_yaml }
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.load(file_path)
|
33
|
+
new.tap { |store| store.instance_variable_set('@db', YAML.load_file(file_path)) }
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def deserialize(data)
|
39
|
+
YAML.load(data)
|
40
|
+
end
|
41
|
+
|
42
|
+
def serialize(data)
|
43
|
+
YAML.dump(data)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class EventStore < MemoryStore
|
48
|
+
class Collection
|
49
|
+
extend Forwardable
|
50
|
+
include Enumerable
|
51
|
+
|
52
|
+
attr_reader :collection
|
53
|
+
def_delegators :collection, :each
|
54
|
+
|
55
|
+
def initialize(collection)
|
56
|
+
@collection = collection
|
57
|
+
end
|
58
|
+
|
59
|
+
def since(time)
|
60
|
+
Collection.new collection.select { |item| item.timestamp <= time }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def get(key, _ = {})
|
65
|
+
Collection.new db[key].map { |item| deserialize(item) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def set(key, value)
|
69
|
+
db[key].push serialize(value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def flush
|
73
|
+
@db = Hash.new { |h, k| h[k] = [] }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'jouba/aggregate'
|
3
|
+
|
4
|
+
describe 'Aggregate' do
|
5
|
+
class Customer < Hashie::Dash
|
6
|
+
include Jouba::Aggregate.new(prefix: :on)
|
7
|
+
property :uuid
|
8
|
+
property :name
|
9
|
+
|
10
|
+
def self.create(attributes)
|
11
|
+
Customer.new(uuid: SecureRandom.uuid).tap do |customer|
|
12
|
+
customer.create(attributes.merge(uuid: customer.uuid))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def create(attributes)
|
17
|
+
emit(:created, attributes)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def on_created(attributes)
|
23
|
+
update_attributes!(attributes)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:name) { 'foo' }
|
28
|
+
|
29
|
+
describe 'create new customer' do
|
30
|
+
subject { Customer.create(name: name) }
|
31
|
+
|
32
|
+
it 'creates a customer' do
|
33
|
+
expect(subject).to be_a Customer
|
34
|
+
expect(subject.name).to eq name
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'increased the stream stack' do
|
38
|
+
expect(Customer.stream(subject.uuid).size).to eq 1
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'it findable' do
|
42
|
+
expect(Customer.find(subject.uuid)).to eq subject
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'with cache' do
|
46
|
+
before do
|
47
|
+
Jouba.config.Cache = Jouba::Cache::Memory.new
|
48
|
+
end
|
49
|
+
|
50
|
+
after do
|
51
|
+
Jouba.config.Cache = Jouba::Cache::Null.new
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'when there is something in the cache' do
|
55
|
+
before { Customer.create(name: name) }
|
56
|
+
after { 10.times { Customer.find(subject.uuid) } }
|
57
|
+
|
58
|
+
it 'dont uses the cache' do
|
59
|
+
expect(Customer).not_to receive(:replay)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when there is nothing in the cache' do
|
64
|
+
let(:customer) { Customer.create(name: name) }
|
65
|
+
let(:key) { Customer.key_from_uuid(customer.uuid) }
|
66
|
+
let(:uuid) { customer.uuid }
|
67
|
+
|
68
|
+
before do
|
69
|
+
expect(Jouba.Cache.get(key)).not_to eq nil
|
70
|
+
Jouba.Cache.store.flush
|
71
|
+
expect(Jouba.Cache.get(key)).to eq nil
|
72
|
+
end
|
73
|
+
|
74
|
+
after { 10.times { Customer.find(uuid) } }
|
75
|
+
|
76
|
+
it 'uses the cache once' do
|
77
|
+
expect { |b| Jouba.Cache.fetch(key, &b) }.to yield_control.exactly(1)
|
78
|
+
expect(Jouba.Cache).to receive(:fetch).exactly(10)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -1,74 +1,115 @@
|
|
1
|
-
require 'ostruct'
|
2
1
|
require 'spec_helper'
|
2
|
+
require 'jouba/aggregate'
|
3
3
|
|
4
4
|
describe Jouba::Aggregate do
|
5
|
-
let(:
|
5
|
+
let(:uuid) { '123' }
|
6
|
+
let(:attributes) { { name: 'bar' } }
|
7
|
+
let(:name) { :created }
|
8
|
+
let(:event) { Jouba::Event.new(key: target.to_key, name: name, data: attributes) }
|
9
|
+
let(:listener) { double(:listener) }
|
6
10
|
|
7
|
-
|
11
|
+
let(:target_class) do
|
12
|
+
Class.new(OpenStruct) do
|
13
|
+
include Jouba::Aggregate.new(prefix: :on)
|
14
|
+
include Wisper::Publisher
|
8
15
|
|
9
|
-
|
10
|
-
|
16
|
+
def create(attributes)
|
17
|
+
emit(:created, attributes)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
let(:target) { target_class.new(uuid: uuid) }
|
22
|
+
|
23
|
+
describe '.initialize' do
|
24
|
+
it 'has instance methods of an aggreate' do
|
25
|
+
described_class::InstanceMethods.public_instance_methods.each do |meth|
|
26
|
+
expect(target_class.new).to respond_to(meth)
|
27
|
+
end
|
28
|
+
end
|
11
29
|
|
12
|
-
it '
|
13
|
-
|
14
|
-
|
30
|
+
it 'has class methods of an aggreate' do
|
31
|
+
described_class::ClassMethods.public_instance_methods.each do |meth|
|
32
|
+
expect(target_class).to respond_to(meth)
|
33
|
+
end
|
15
34
|
end
|
16
35
|
end
|
17
36
|
|
18
|
-
describe '
|
19
|
-
|
20
|
-
let(:uuid) { '123' }
|
21
|
-
let(:events) { [double(:event)] }
|
37
|
+
describe '#emit(name, *args)' do
|
38
|
+
after { target.create(attributes) }
|
22
39
|
|
23
|
-
|
24
|
-
expect(
|
25
|
-
|
26
|
-
expect(
|
27
|
-
aggregate_class.build_from_events(uuid, events)
|
40
|
+
before do
|
41
|
+
expect(Jouba.Event).to receive(:new)
|
42
|
+
.with(key: target.to_key, name: name, data: [attributes]).and_return(event)
|
43
|
+
expect(target).to receive(:"on_#{name}").with(*attributes)
|
28
44
|
end
|
29
45
|
|
30
|
-
|
31
|
-
|
46
|
+
it 'apply the event' do
|
47
|
+
expect(target).to receive(:apply_event).with(event).and_call_original
|
48
|
+
end
|
32
49
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
50
|
+
it 'refresh the cache' do
|
51
|
+
expect(Jouba.Cache).to receive(:refresh).with(target.to_key, target)
|
52
|
+
.and_yield.and_call_original
|
53
|
+
end
|
38
54
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
expect(aggregate).to receive(:[]=).with(:uuid, uuid)
|
43
|
-
expect(aggregate).to receive(:subscribe).with(observer)
|
44
|
-
aggregate_class.build_from_events(uuid, events)
|
45
|
-
end
|
55
|
+
it 'publish an event' do
|
56
|
+
target.subscribe(listener, prefix: :on)
|
57
|
+
expect(listener).to receive(:"on_#{name}").with(attributes)
|
46
58
|
end
|
59
|
+
end
|
47
60
|
|
61
|
+
describe '#to_key' do
|
62
|
+
let(:uuid) { 123 }
|
63
|
+
it 'delegates to the class method' do
|
64
|
+
expect(target_class).to receive(:key_from_uuid).with(target.uuid)
|
65
|
+
target.to_key
|
66
|
+
end
|
48
67
|
end
|
49
68
|
|
50
|
-
describe '
|
69
|
+
describe '#replay(event)' do
|
70
|
+
after { target.replay(event) }
|
71
|
+
it 'calls the callback_method with the right params' do
|
72
|
+
expect(target).to receive(:"on_#{name}").with(*attributes)
|
73
|
+
end
|
51
74
|
end
|
52
75
|
|
53
|
-
describe '
|
54
|
-
let(:
|
76
|
+
describe '.replay(events)' do
|
77
|
+
let(:events) { [event, event] }
|
78
|
+
|
79
|
+
after { target_class.replay(events) }
|
80
|
+
before { expect(target_class).to receive(:new).and_return(target) }
|
55
81
|
|
56
|
-
it '
|
57
|
-
expect(
|
82
|
+
it 'create a new instance of the aggregate and apply all the events' do
|
83
|
+
expect(target).to receive(:replay).with(event).exactly(2).times
|
58
84
|
end
|
59
85
|
end
|
60
86
|
|
61
|
-
describe '
|
62
|
-
let(:
|
63
|
-
let(:
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
expect(
|
69
|
-
expect(Jouba
|
70
|
-
|
71
|
-
|
87
|
+
describe '.find(uuid)' do
|
88
|
+
let(:key) { target_class.key_from_uuid(uuid) }
|
89
|
+
let(:stream) { [] }
|
90
|
+
after { target_class.find(uuid) }
|
91
|
+
|
92
|
+
it 'goes through the cache' do
|
93
|
+
expect(target_class).to receive(:stream).with(uuid).and_return(stream)
|
94
|
+
expect(target_class).to receive(:replay).with(stream)
|
95
|
+
expect(Jouba.Cache).to receive(:fetch).with(key).and_yield.and_call_original
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '.stream(uuid)' do
|
100
|
+
let(:key) { target_class.key_from_uuid(uuid) }
|
101
|
+
let(:params) { {} }
|
102
|
+
after { target_class.stream(uuid, params) }
|
103
|
+
it 'returns the stream from the EventStore' do
|
104
|
+
expect(Jouba.Event).to receive(:stream).with(key, params)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '.key_from_uuid(uuid)' do
|
109
|
+
after { target_class.key_from_uuid(uuid) }
|
110
|
+
|
111
|
+
it 'delegates to the configured key structure' do
|
112
|
+
expect(Jouba.Key).to receive(:serialize).with(target_class.name, uuid)
|
72
113
|
end
|
73
114
|
end
|
74
115
|
end
|
File without changes
|
@@ -1,21 +1,85 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Jouba::Event do
|
4
|
-
let(:
|
5
|
-
let(:
|
6
|
-
let(:
|
4
|
+
let(:key) { 'User.1' }
|
5
|
+
let(:name) { 'created' }
|
6
|
+
let(:data) { { fname: 'John', lname: 'Doe' } }
|
7
|
+
let(:uuid) { UUID.new }
|
8
|
+
let(:attributes) do
|
9
|
+
{ key: key, name: name, data: data }
|
10
|
+
end
|
11
|
+
|
12
|
+
subject { described_class.new(attributes) }
|
7
13
|
|
8
|
-
describe '.
|
14
|
+
describe '.new(attributes)' do
|
9
15
|
before do
|
10
|
-
|
16
|
+
allow(UUID).to receive(:new).at_least(:once).and_return(uuid)
|
11
17
|
end
|
12
18
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
expect(subject.
|
19
|
+
it 'sets the attributes and sets defaults for the other values' do
|
20
|
+
expect(subject.uuid).to eq uuid.to_s
|
21
|
+
expect(subject.version).to eq uuid.version
|
22
|
+
expect(subject.timestamp).to eq uuid.timestamp
|
23
|
+
expect(subject.key).to eq key
|
24
|
+
expect(subject.name).to eq name
|
17
25
|
expect(subject.data).to eq data
|
18
|
-
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.serialize(event)' do
|
30
|
+
before do
|
31
|
+
allow(UUID).to receive(:new).at_least(:once).and_return(uuid)
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:event) { described_class.new(attributes) }
|
35
|
+
let(:expected_serialized) do
|
36
|
+
{
|
37
|
+
key: key,
|
38
|
+
name: name,
|
39
|
+
data: data,
|
40
|
+
uuid: uuid.to_s,
|
41
|
+
version: uuid.version,
|
42
|
+
timestamp: uuid.timestamp
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
subject { described_class.serialize(event) }
|
47
|
+
|
48
|
+
it 'serializes into a hash' do
|
49
|
+
expect(subject).to eq expected_serialized
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '.deserialize(serialized_event)' do
|
54
|
+
let(:event) { described_class.new(attributes) }
|
55
|
+
let(:serialized_event) { described_class.serialize(event) }
|
56
|
+
subject { described_class.deserialize(serialized_event) }
|
57
|
+
|
58
|
+
it 'rebuild an event based on the serialized version' do
|
59
|
+
expect(subject).to eq event
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '.stream(key, params)' do
|
64
|
+
let(:stream) { [described_class.new(attributes), described_class.new(attributes)] }
|
65
|
+
let(:key) { 'key' }
|
66
|
+
let(:params) { { lname: 'foo' } }
|
67
|
+
let(:serialized_stream) { stream.map { |item| described_class.serialize(item) } }
|
68
|
+
|
69
|
+
before { expect(Jouba.Store).to receive(:get).with(key, params).and_return(serialized_stream) }
|
70
|
+
|
71
|
+
it 'returns a stream of events' do
|
72
|
+
expect(described_class.stream(key, params)).to eq stream
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#track' do
|
77
|
+
let(:event) { described_class.new(attributes) }
|
78
|
+
let(:serialized_event) { described_class.serialize(event) }
|
79
|
+
before { expect(Jouba.Store).to receive(:set).with(event.key, serialized_event) }
|
80
|
+
|
81
|
+
it 'persist an event in the store' do
|
82
|
+
event.track
|
19
83
|
end
|
20
84
|
end
|
21
85
|
end
|