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.
@@ -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