jouba 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,13 +1,33 @@
1
1
  module Jouba
2
- class Event < Hashie::Dash
3
- include Hashie::Extensions::IndifferentAccess
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
- property :name, required: true
6
- property :data, required: true
7
- property :occured_at, default: -> { Time.now.utc }
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 self.build(event_name, data)
10
- new(name: event_name, data: data)
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
@@ -0,0 +1,11 @@
1
+ module Jouba
2
+ class Key < Struct.new(:name, :id)
3
+ def self.serialize(name, id)
4
+ "#{name}.#{id}"
5
+ end
6
+
7
+ def self.deserialize(key)
8
+ new(*key.split('.')[(0..1)])
9
+ end
10
+ end
11
+ end
@@ -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(:aggregate_class) { Class.new { include Jouba::Aggregate } }
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
- subject { aggregate_class }
11
+ let(:target_class) do
12
+ Class.new(OpenStruct) do
13
+ include Jouba::Aggregate.new(prefix: :on)
14
+ include Wisper::Publisher
8
15
 
9
- describe '.find(id)' do
10
- let(:id) { 2 }
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 'query the store' do
13
- expect(Jouba).to receive(:find).with(aggregate_class, id)
14
- subject.find(id)
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 '.build_from_events(uuid, events)' do
19
- let(:aggregate) { aggregate_class.new }
20
- let(:uuid) { '123' }
21
- let(:events) { [double(:event)] }
37
+ describe '#emit(name, *args)' do
38
+ after { target.create(attributes) }
22
39
 
23
- it 'build the aggregate by applying the events' do
24
- expect(aggregate_class).to receive(:new).and_return(aggregate)
25
- expect(aggregate).to receive(:[]=).with(:uuid, uuid)
26
- expect(aggregate).to receive(:apply_events).with(events)
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
- context 'when after_initialize_blocks is not empty' do
31
- let(:observer) { double(:observer) }
46
+ it 'apply the event' do
47
+ expect(target).to receive(:apply_event).with(event).and_call_original
48
+ end
32
49
 
33
- before do
34
- aggregate_class.after_initialize do |aggregate|
35
- aggregate.subscribe(observer)
36
- end
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
- it 'apply the blocks once initialized' do
40
- expect(aggregate_class).to receive(:new).and_return(aggregate)
41
- expect(aggregate).to receive(:apply_events).with(events)
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 '.after_initialize(&block)' do
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 '#uuid' do
54
- let(:uuids) { (1..3).map { aggregate_class.new.uuid } }
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 'return a different uuid for each instance' do
57
- expect(uuids.uniq.size).to eq 3
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 '#commit(aggregate, event)' do
62
- let(:aggregate) { aggregate_class.new }
63
- let(:data) { { value: 10, meta: OpenStruct.new(foo: 'bar') } }
64
- let(:event_name) { 'add_credit' }
65
- let(:event) { Jouba::Event.new(name: event_name, data: data) }
66
-
67
- it 'append the event to the store' do
68
- expect(Jouba.stores[:events]).to receive(:append_events).with(aggregate, event)
69
- expect(Jouba::Event).to receive(:build).with(event_name, data).and_return(event)
70
- expect(aggregate).to receive(event_name).with(data)
71
- aggregate.commit(event_name, data)
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(:event_name) { 'event_name' }
5
- let(:data) { [:foo, 1, 'bar', { foo: 'bar' }, [1, 2]] }
6
- let(:occured_at) { double(:time) }
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 '.build(event_name, data)' do
14
+ describe '.new(attributes)' do
9
15
  before do
10
- Time.stub_chain(:now, :utc).and_return(occured_at)
16
+ allow(UUID).to receive(:new).at_least(:once).and_return(uuid)
11
17
  end
12
18
 
13
- subject { described_class.build(event_name, data) }
14
-
15
- it 'build an event' do
16
- expect(subject.name).to eq event_name
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
- expect(subject.occured_at).to eq occured_at
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