entity_store 0.0.2

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,61 @@
1
+ module EntityStore
2
+ require 'logger'
3
+ require 'entity_store/entity'
4
+ require 'entity_store/entity_value'
5
+ require 'entity_store/event'
6
+ require 'entity_store/store'
7
+ require 'entity_store/external_store'
8
+ require 'entity_store/event_data_object'
9
+ require 'entity_store/mongo_entity_store'
10
+ require 'entity_store/event_bus'
11
+ require 'entity_store/not_found'
12
+
13
+ class << self
14
+ def setup
15
+ yield self
16
+ end
17
+
18
+ def connection_profile
19
+ @_connection_profile
20
+ end
21
+
22
+ def connection_profile=(value)
23
+ @_connection_profile = value
24
+ end
25
+
26
+ def external_connection_profile
27
+ @_external_connection_profile
28
+ end
29
+
30
+ def external_connection_profile=(value)
31
+ @_external_connection_profile = value
32
+ end
33
+
34
+ def event_subscribers
35
+ @_event_subscribers ||=[]
36
+ end
37
+
38
+ def log_level
39
+ @_log_level ||= Logger::INFO
40
+ end
41
+
42
+ def log_level=(value)
43
+ @_log_level = value
44
+ end
45
+
46
+ def logger
47
+ unless @_logger
48
+ @_logger = Logger.new(STDOUT)
49
+ @_logger.progname = "Entity_Store"
50
+ @_logger.level = log_level
51
+ end
52
+ @_logger
53
+ end
54
+
55
+ def logger=(value)
56
+ @_logger = value
57
+ end
58
+ end
59
+
60
+
61
+ end
File without changes
@@ -0,0 +1,31 @@
1
+ module EntityStore
2
+ module Entity
3
+ attr_accessor :id
4
+ attr_writer :version
5
+
6
+ def initialize(attr={})
7
+ attr.each_pair { |k,v| self.send("#{k}=", v) }
8
+ end
9
+
10
+ def type
11
+ self.class.name
12
+ end
13
+
14
+ def version
15
+ @version ||= 1
16
+ end
17
+
18
+ def pending_events
19
+ @pending_events ||= []
20
+ end
21
+
22
+ def record_event(event)
23
+ apply_event(event)
24
+ pending_events<<event
25
+ end
26
+
27
+ def apply_event(event)
28
+ event.apply(self)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ module EntityStore
2
+ module EntityValue
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+ extend ClassMethods
6
+ end
7
+ end
8
+
9
+ def initialize(attr={})
10
+ attr.each_pair { |k,v| self.send("#{k}=", v) if self.respond_to? "#{k}=" }
11
+ end
12
+
13
+ def attributes
14
+ Hash[*public_methods.select {|m| m =~ /\w\=$/}.collect do |m|
15
+ attribute_name = m.to_s.chop.to_sym
16
+ [attribute_name, send(attribute_name).respond_to?(:attributes) ? send(attribute_name).attributes : send(attribute_name)]
17
+ end.flatten]
18
+ end
19
+
20
+ def ==(other)
21
+ attributes.each_key do |attr|
22
+ return false unless other.respond_to?(attr) && send(attr) == other.send(attr)
23
+ end
24
+ return true
25
+ end
26
+
27
+ module ClassMethods
28
+ def entity_value_attribute(name, klass)
29
+ define_method(name) { instance_variable_get("@#{name}") }
30
+ define_method("#{name}=") do |value|
31
+ instance_variable_set("@#{name}", value.is_a?(Hash) ? klass.new(value) : value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,56 @@
1
+ module EntityStore
2
+ module Event
3
+ attr_accessor :entity_id
4
+
5
+ def initialize(attrs={})
6
+ attrs.each_pair do |key, value|
7
+ send("#{key}=", value) if respond_to?("#{key}=")
8
+ end
9
+ end
10
+
11
+ def receiver_name
12
+ elements = self.class.name.split('::')
13
+ elements[elements.count - 1].
14
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
15
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
16
+ tr("-", "_").
17
+ downcase
18
+ end
19
+
20
+ def attributes
21
+ Hash[*public_methods.select {|m| m =~ /\w\=$/}.collect do |m|
22
+ attribute_name = m.to_s.chop.to_sym
23
+ [attribute_name, send(attribute_name).respond_to?(:attributes) ? send(attribute_name).attributes : send(attribute_name)]
24
+ end.flatten]
25
+ end
26
+
27
+ def self.included(klass)
28
+ klass.class_eval do
29
+ extend ClassMethods
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ def time_attribute(*names)
35
+ class_eval do
36
+ names.each do |name|
37
+ define_method "#{name}=" do |value|
38
+ require 'time'
39
+ instance_variable_set("@#{name}", value.kind_of?(String) ? Time.parse(value) : value)
40
+ end
41
+ define_method name do
42
+ instance_variable_get "@#{name}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def entity_value_attribute(name, klass)
49
+ define_method(name) { instance_variable_get("@#{name}") }
50
+ define_method("#{name}=") do |value|
51
+ instance_variable_set("@#{name}", value.is_a?(Hash) ? klass.new(value) : value)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,34 @@
1
+ module EntityStore
2
+ class EventBus
3
+ class << self
4
+ def publish(entity_type, event)
5
+ publish_externally entity_type, event
6
+
7
+ subscribers_to(event.receiver_name).each do |s|
8
+ begin
9
+ s.new.send(event.receiver_name, event)
10
+ EntityStore.logger.debug { "called #{s.name}##{event.receiver_name} with #{event.inspect}" }
11
+ rescue => e
12
+ EntityStore.logger.error { "#{e.message} when calling #{s.name}##{event.receiver_name} with #{event.inspect}" }
13
+ end
14
+ end
15
+ end
16
+
17
+ def subscribers_to(event_name)
18
+ subscribers.select { |s| s.instance_methods.include?(event_name.to_sym) }
19
+ end
20
+
21
+ def subscribers
22
+ EntityStore.event_subscribers
23
+ end
24
+
25
+ def publish_externally(entity_type, event)
26
+ external_store.add_event(entity_type, event)
27
+ end
28
+
29
+ def external_store
30
+ @_external_store ||= ExternalStore.new
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ module EntityStore
2
+ class EventDataObject
3
+ attr_reader :attrs
4
+
5
+ def initialize(attrs={})
6
+ @attrs = attrs
7
+ end
8
+
9
+ def id
10
+ attrs['_id']
11
+ end
12
+
13
+ def entity_type
14
+ attrs['_entity_type']
15
+ end
16
+
17
+ def type
18
+ attrs['_type']
19
+ end
20
+
21
+ def [](key)
22
+ attrs[key]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ require 'mongo'
2
+ require 'uri'
3
+
4
+ module EntityStore
5
+ class ExternalStore
6
+ include Mongo
7
+
8
+ def open_connection
9
+ @db ||= open_store
10
+ end
11
+
12
+ def open_store
13
+ uri = URI.parse(EntityStore.external_connection_profile)
14
+ Connection.from_uri(EntityStore.external_connection_profile).db(uri.path.gsub(/^\//, ''))
15
+ end
16
+
17
+ def collection
18
+ @_collection ||= open_connection['events']
19
+ end
20
+
21
+ def ensure_indexes
22
+ collection.ensure_index([['_type', Mongo::ASCENDING], ['_id', Mongo::ASCENDING]])
23
+ end
24
+
25
+ def add_event(entity_type, event)
26
+ collection.insert({
27
+ '_entity_type' => entity_type, '_type' => event.class.name
28
+ }.merge(event.attributes)
29
+ )
30
+ end
31
+
32
+ def get_events(opts={})
33
+ query = {}
34
+ query['_id'] = { '$gt' => opts[:after] } if opts[:after]
35
+ query['_type'] = opts[:type] if opts[:type]
36
+
37
+ options = {:sort => [['_id', -1]]}
38
+ options[:limit] = opts[:limit] || 100
39
+
40
+ collection.find(query, options).collect { |e| EventDataObject.new(e)}
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,86 @@
1
+ require 'mongo'
2
+ require 'uri'
3
+
4
+ module EntityStore
5
+ class MongoEntityStore
6
+ include Mongo
7
+
8
+ def open_connection
9
+ @db ||= open_store
10
+ end
11
+
12
+ def open_store
13
+ uri = URI.parse(EntityStore.connection_profile)
14
+ connection = Connection.from_uri(EntityStore.connection_profile, :connecttimeoutms => connect_timeout)
15
+ connection.db(uri.path.gsub(/^\//, ''))
16
+ end
17
+
18
+ def connect_timeout
19
+ ENV['ENTITY_STORE_CONNECT_TIMEOUT'] || '2000'
20
+ end
21
+
22
+ def entities
23
+ @entities_collection ||= open_connection['entities']
24
+ end
25
+
26
+ def events
27
+ @events_collection ||= open_connection['entity_events']
28
+ end
29
+
30
+ def ensure_indexes
31
+ events_collection.ensure_index([['entity_id', Mongo::ASCENDING], ['_id', Mongo::ASCENDING]])
32
+ end
33
+
34
+ def add_entity(entity)
35
+ entities.insert('_type' => entity.class.name, 'version' => entity.version).to_s
36
+ end
37
+
38
+ def save_entity(entity)
39
+ entities.update({'_id' => BSON::ObjectId.from_string(entity.id)}, { '$set' => { 'version' => entity.version } })
40
+ end
41
+
42
+ def add_event(event)
43
+ events.insert({'_type' => event.class.name, '_entity_id' => BSON::ObjectId.from_string(event.entity_id) }.merge(event.attributes) ).to_s
44
+ end
45
+
46
+ def get_entity!(id)
47
+ get_entity(id, true)
48
+ end
49
+
50
+ def get_entity(id, raise_exception=false)
51
+ begin
52
+ if attrs = entities.find('_id' => BSON::ObjectId.from_string(id)).first
53
+ get_type_constant(attrs['_type']).new('id' => id, 'version' => attrs['version'])
54
+ else
55
+ if raise_exception
56
+ raise NotFound.new(id)
57
+ else
58
+ return nil
59
+ end
60
+ end
61
+ rescue BSON::InvalidObjectId
62
+ if raise_exception
63
+ raise NotFound.new(id)
64
+ else
65
+ return nil
66
+ end
67
+ end
68
+ end
69
+
70
+ def get_events(id)
71
+ events.find('_entity_id' => BSON::ObjectId.from_string(id)).collect do |attrs|
72
+ begin
73
+ get_type_constant(attrs['_type']).new(attrs)
74
+ rescue => e
75
+ logger = Logger.new(STDERR)
76
+ logger.error "Error loading type #{attrs['_type']}"
77
+ nil
78
+ end
79
+ end.select { |e| !e.nil? }
80
+ end
81
+
82
+ def get_type_constant(type_name)
83
+ type_name.split('::').inject(Object) {|obj, name| obj.const_get(name) }
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,7 @@
1
+ module EntityStore
2
+ class NotFound < StandardError
3
+ def initialise(id)
4
+ super("no item with id #{id} could be found")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,56 @@
1
+ module EntityStore
2
+ class Store
3
+ def storage_client
4
+ @storage_client || MongoEntityStore.new
5
+ end
6
+
7
+ def add(entity)
8
+ entity.id = storage_client.add_entity(entity)
9
+ add_events(entity)
10
+ return entity
11
+ rescue => e
12
+ EntityStore.logger.error { "Store#add error: #{e.inspect} - #{entity.inspect}" }
13
+ raise e
14
+ end
15
+
16
+ def save(entity)
17
+ # need to look at concurrency if we start storing version on client
18
+ entity.version += 1
19
+ storage_client.save_entity(entity)
20
+ add_events(entity)
21
+ return entity
22
+ rescue => e
23
+ EntityStore.logger.error { "Store#save error: #{e.inspect} - #{entity.inspect}" }
24
+ raise e
25
+ end
26
+
27
+ def add_events(entity)
28
+ entity.pending_events.each do |e|
29
+ e.entity_id = entity.id.to_s
30
+ storage_client.add_event(e)
31
+ end
32
+ entity.pending_events.each {|e| EventBus.publish(entity.type, e) }
33
+ end
34
+
35
+ def get!(id)
36
+ get(id, true)
37
+ end
38
+
39
+ def get(id, raise_exception=false)
40
+ if entity = storage_client.get_entity(id, raise_exception)
41
+ storage_client.get_events(id).each { |e| e.apply(entity) }
42
+ end
43
+ return entity
44
+ end
45
+
46
+ # Public : USE AT YOUR PERIL this clears the ENTIRE data store
47
+ #
48
+ # Returns nothing
49
+ def clear_all
50
+ storage_client.entities.drop
51
+ storage_client.events.drop
52
+ @storage_client = nil
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module EntityStore
2
+ VERSION = "0.0.2".freeze
3
+ end
@@ -0,0 +1,7 @@
1
+ require "#{Rake.application.original_dir}/lib/entity_store"
2
+
3
+ namespace :entity_store do
4
+ task :ensure_indexes do
5
+ EntityStore::MongoEntityStore.new.ensure_indexes
6
+ end
7
+ end
@@ -0,0 +1,77 @@
1
+ require "spec_helper"
2
+
3
+ class NestedEntityValue
4
+ include EntityValue
5
+ attr_accessor :street, :town
6
+ end
7
+
8
+ class DummyEntityValue
9
+ include EntityValue
10
+ attr_accessor :name
11
+ entity_value_attribute :home, NestedEntityValue
12
+ end
13
+
14
+ describe EntityValue do
15
+ before(:each) do
16
+ @name = random_string
17
+ @home = random_string
18
+ end
19
+ describe "#initialize" do
20
+ before(:each) do
21
+ @value = DummyEntityValue.new(:name => @name, :home => @home)
22
+ end
23
+ it "sets the name" do
24
+ @value.name.should eq(@name)
25
+ end
26
+ it "sets the home" do
27
+ @value.home.should eq(@home)
28
+ end
29
+ end
30
+
31
+ describe "#attributes" do
32
+ before(:each) do
33
+ @value = DummyEntityValue.new(:name => @name, :home => @home)
34
+ end
35
+ it "should return hash of attributes" do
36
+ @value.attributes.should eq({:name => @name, :home => @home})
37
+ end
38
+ context "nested attributes" do
39
+ before(:each) do
40
+ @street = random_string
41
+ @town = random_string
42
+ @value.home = NestedEntityValue.new(:street => @street, :town => @town)
43
+ end
44
+ it "should return a hash containing the nested attribute" do
45
+ @value.attributes.should eq({:name => @name, :home => {:street => @street, :town => @town}})
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ describe "#==" do
52
+
53
+ subject { @this == @other }
54
+
55
+ context "when values are equal" do
56
+ before(:each) do
57
+ @this = DummyEntityValue.new(:name => random_string)
58
+ @other = DummyEntityValue.new(:name => @this.name)
59
+ end
60
+
61
+ it "should be true" do
62
+ subject.should be_true
63
+ end
64
+ end
65
+
66
+ context "when values are not equal" do
67
+ before(:each) do
68
+ @this = DummyEntityValue.new(:name => random_string)
69
+ @other = DummyEntityValue.new(:name => random_string)
70
+ end
71
+
72
+ it "should be false" do
73
+ subject.should be_false
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyEvent
4
+ include Event
5
+ attr_accessor :name
6
+ end
7
+
8
+ class DummySubscriber
9
+ def dummy_event
10
+
11
+ end
12
+ end
13
+
14
+ describe EventBus do
15
+ before(:each) do
16
+ @entity_type = random_string
17
+ @event = DummyEvent.new(:name => random_string)
18
+ end
19
+ describe ".publish" do
20
+ before(:each) do
21
+ @subscriber = mock("Subscriber", :dummy_event => true)
22
+ DummySubscriber.stub(:new) { @subscriber }
23
+ @subscriber_class2 = mock("SubscriberClass", :instance_methods => ['bilge'], :name => "SubscriberClass")
24
+ EventBus.stub(:subscribers).and_return([DummySubscriber, @subscriber_class2])
25
+ EventBus.stub(:publish_externally)
26
+ end
27
+
28
+ subject { EventBus.publish(@entity_type, @event) }
29
+
30
+ it "calls the receiver method on the subscriber" do
31
+ @subscriber.should_receive(:dummy_event).with(@event)
32
+ subject
33
+ end
34
+ it "should not create an instance of a class without the receiver method" do
35
+ @subscriber_class2.should_not_receive(:new)
36
+ subject
37
+ end
38
+ it "publishes event to the external event push" do
39
+ EventBus.should_receive(:publish_externally).with(@entity_type, @event)
40
+ subject
41
+ end
42
+ end
43
+
44
+ describe ".publish_externally" do
45
+ before(:each) do
46
+ @external_store = mock(ExternalStore)
47
+ EventBus.stub(:external_store) { @external_store }
48
+ end
49
+
50
+ subject { EventBus.publish_externally @entity_type, @event }
51
+
52
+ it "should publish to the external store" do
53
+ @external_store.should_receive(:add_event).with(@entity_type, @event)
54
+ subject
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyValue
4
+ include EntityValue
5
+ attr_accessor :town, :county
6
+
7
+ end
8
+
9
+ class DummyEvent
10
+ include Event
11
+ attr_accessor :name
12
+ time_attribute :updated_at, :sent_at
13
+ entity_value_attribute :address, DummyValue
14
+ end
15
+
16
+ describe Event do
17
+ before(:each) do
18
+ @id = random_integer
19
+ @name = random_string
20
+ @time = random_time
21
+ @town = random_string
22
+ @county = random_string
23
+ end
24
+ describe "#initialize" do
25
+
26
+ subject { DummyEvent.new({:entity_id => @id, :name => @name, :updated_at => @time, :sent_at => nil, :address => {:town => @town, :county => @county}})}
27
+
28
+ it "should set entity_id" do
29
+ subject.entity_id.should eq(@id)
30
+ end
31
+ it "should set name" do
32
+ subject.name.should eq(@name)
33
+ end
34
+ it "should set updated_at" do
35
+ subject.updated_at.should eq(@time)
36
+ end
37
+ it "should set town" do
38
+ subject.address.town.should eq(@town)
39
+ end
40
+ it "should set county" do
41
+ subject.address.county.should eq(@county)
42
+ end
43
+ end
44
+
45
+ describe "#attributes" do
46
+ before(:each) do
47
+ @event = DummyEvent.new(:entity_id => @id, :name => @name, :updated_at => @time, :address => DummyValue.new(:town => @town, :county => @county))
48
+ end
49
+
50
+ subject { @event.attributes }
51
+
52
+ it "returns a hash of the attributes" do
53
+ subject.should eq({:entity_id => @id, :name => @name, :updated_at => @time, :sent_at => nil, :address => {:town => @town, :county => @county}})
54
+ end
55
+ end
56
+
57
+ describe ".time_attribute" do
58
+ before(:each) do
59
+ @event = DummyEvent.new
60
+ @time = random_time
61
+ end
62
+ context "updated_at" do
63
+ subject { @event.updated_at = @time.to_s }
64
+
65
+ it "parses the time field when added as a string" do
66
+ subject
67
+ @event.updated_at.to_i.should eq(@time.to_i)
68
+ end
69
+ end
70
+ context "sent_at" do
71
+ subject { @event.updated_at = @time.to_s }
72
+
73
+ it "parses the time field when added as a string" do
74
+ subject
75
+ @event.updated_at.to_i.should eq(@time.to_i)
76
+ end
77
+ end
78
+ end
79
+
80
+ describe ".value_attribute" do
81
+ before(:each) do
82
+ @event = DummyEvent.new
83
+ end
84
+ context "assign a value" do
85
+ before(:each) do
86
+ @value = DummyValue.new(:town => random_string, :county => random_string)
87
+ @event.address = @value
88
+ end
89
+ it "assigns town" do
90
+ @event.address.town.should eq(@value.town)
91
+ end
92
+ it "assigns county" do
93
+ @event.address.county.should eq(@value.county)
94
+ end
95
+ end
96
+ context "assign a hash" do
97
+ before(:each) do
98
+ @hash = { 'town' => random_string, 'county' => random_string }
99
+ @event.address = @hash
100
+ end
101
+ it "assigns town" do
102
+ @event.address.town.should eq(@hash['town'])
103
+ end
104
+ it "assigns county" do
105
+ @event.address.county.should eq(@hash['county'])
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyEvent
4
+ include Event
5
+ attr_accessor :name
6
+ end
7
+
8
+ class DummyEventTwo
9
+ include Event
10
+ attr_accessor :name
11
+ end
12
+
13
+ describe ExternalStore do
14
+ before(:each) do
15
+ EntityStore.external_connection_profile = "mongodb://localhost/external_entity_store_default"
16
+ ExternalStore.new.collection.drop
17
+ @store = ExternalStore.new
18
+ end
19
+ describe "#add_event" do
20
+ before(:each) do
21
+ @entity_type = random_string
22
+ @event = DummyEvent.new(:name => random_string, :entity_id => random_object_id)
23
+ end
24
+
25
+ subject { @store.add_event(@entity_type, @event) }
26
+
27
+ it "creates a record in the collection" do
28
+ subject
29
+ item = @store.collection.find_one
30
+ item['_entity_type'].should eq(@entity_type)
31
+ item['_type'].should eq(@event.class.name)
32
+ item['name'].should eq(@event.name)
33
+ item['entity_id'].should eq(@event.entity_id)
34
+ end
35
+ end
36
+
37
+ describe "#get_events" do
38
+ before(:each) do
39
+ @entity_type = random_string
40
+ @events = [
41
+ DummyEvent.new(:name => random_string, :entity_id => random_object_id),
42
+ DummyEventTwo.new(:name => random_string, :entity_id => random_object_id),
43
+ DummyEvent.new(:name => random_string, :entity_id => random_object_id),
44
+ DummyEventTwo.new(:name => random_string, :entity_id => random_object_id),
45
+ DummyEvent.new(:name => random_string, :entity_id => random_object_id)
46
+ ]
47
+
48
+ @events.each { |e| @store.add_event(@entity_type, e)}
49
+ end
50
+
51
+ context "when no options" do
52
+
53
+ subject { @store.get_events }
54
+
55
+ it "returns all of the events" do
56
+ subject.count.should eq(@events.count)
57
+ end
58
+ end
59
+
60
+ context "when options passed" do
61
+ subject { @store.get_events(@options) }
62
+
63
+ context "when limit option passed" do
64
+ before(:each) do
65
+ @options = {:limit => 3}
66
+ end
67
+
68
+ it "returns limited records records" do
69
+ subject.count.should eq(@options[:limit])
70
+ end
71
+ end
72
+
73
+ context "when after index passed" do
74
+ before(:each) do
75
+ items = @store.get_events(:limit => 3)
76
+ @options = {:after => items[2].id}
77
+ end
78
+
79
+ it "returns limited records records" do
80
+ subject.count.should eq(2)
81
+ end
82
+ end
83
+
84
+ context "when type passed" do
85
+ before(:each) do
86
+ @options = {:type => @events[2].class.name}
87
+ end
88
+
89
+ it "returns type records records" do
90
+ subject.count.should eq(3)
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ module Level1
4
+ module Level2
5
+ class MyClass
6
+ end
7
+ end
8
+ end
9
+
10
+ describe MongoEntityStore do
11
+ before(:each) do
12
+ EntityStore.connection_profile = "mongodb://localhost/entity_store_default"
13
+ @store = MongoEntityStore.new
14
+ end
15
+ describe "#get_entity!" do
16
+ context "when invalid id format passed" do
17
+
18
+ subject { @store.get_entity!(random_string) }
19
+
20
+ it "should raise not found" do
21
+ expect { subject }.to raise_error(NotFound)
22
+ end
23
+ end
24
+ context "when valid id format passed but no object exists" do
25
+ before(:each) do
26
+ @store = MongoEntityStore.new
27
+ end
28
+
29
+ subject { @store.get_entity!(random_object_id) }
30
+
31
+ it "should raise not found" do
32
+ expect { subject }.to raise_error(NotFound)
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ describe "get_type_constant" do
39
+
40
+ subject { @store.get_type_constant('Level1::Level2::MyClass') }
41
+
42
+ it "should be an Level1::Level2::MyClass" do
43
+ subject.should eq(Level1::Level2::MyClass)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyEntity
4
+ include Entity
5
+
6
+ attr_accessor :name
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+ end
12
+
13
+ describe Store do
14
+ describe "#add" do
15
+ before(:each) do
16
+ @new_id = random_string
17
+ @entity = DummyEntity.new(random_string)
18
+ @storage_client = mock("StorageClient", :add_entity => @new_id)
19
+ @store = Store.new
20
+ @store.stub(:add_events)
21
+ @store.stub(:storage_client) { @storage_client }
22
+ end
23
+
24
+ subject { @store.add(@entity) }
25
+
26
+ it "adds the new entity to the store" do
27
+ @storage_client.should_receive(:add_entity).with(@entity)
28
+ subject
29
+ end
30
+ it "adds events" do
31
+ @store.should_receive(:add_events).with(@entity)
32
+ subject
33
+ end
34
+ it "returns a reference to the ride" do
35
+ subject.should eq(@entity)
36
+ end
37
+ end
38
+
39
+ describe "#add_events" do
40
+ before(:each) do
41
+ @entity = DummyEntity.new(random_string)
42
+ @entity.id = random_string
43
+ @entity.pending_events << mock(Event, :entity_id= => true)
44
+ @entity.pending_events << mock(Event, :entity_id= => true)
45
+ @storage_client = mock("StorageClient", :add_event => true)
46
+ @store = Store.new
47
+ @store.stub(:storage_client) { @storage_client }
48
+ EventBus.stub(:publish)
49
+ end
50
+
51
+ subject { @store.add_events(@entity) }
52
+
53
+ it "adds each of the events" do
54
+ @entity.pending_events.each do |e|
55
+ @storage_client.should_receive(:add_event).with(e)
56
+ end
57
+ subject
58
+ end
59
+ it "should assign the new entity_id to each event" do
60
+ @entity.pending_events.each do |e|
61
+ e.should_receive(:entity_id=).with(@entity.id)
62
+ end
63
+ subject
64
+ end
65
+ it "publishes each event to the EventBus" do
66
+ @entity.pending_events.each do |e|
67
+ EventBus.should_receive(:publish).with(@entity.type, e)
68
+ end
69
+ subject
70
+ end
71
+
72
+ end
73
+
74
+ describe "#save" do
75
+ before(:each) do
76
+ @new_id = random_string
77
+ @entity = DummyEntity.new(random_string)
78
+ @storage_client = mock("StorageClient", :save_entity => true)
79
+ @store = Store.new
80
+ @store.stub(:add_events)
81
+ @store.stub(:storage_client) { @storage_client }
82
+ end
83
+
84
+ subject { @store.save(@entity) }
85
+
86
+ it "increments the entity version number" do
87
+ @entity.should_receive(:version=).with(@entity.version + 1)
88
+ subject
89
+ end
90
+ it "save the new entity to the store" do
91
+ @storage_client.should_receive(:save_entity).with(@entity)
92
+ subject
93
+ end
94
+ it "adds events" do
95
+ @store.should_receive(:add_events).with(@entity)
96
+ subject
97
+ end
98
+ it "returns a reference to the ride" do
99
+ subject.should eq(@entity)
100
+ end
101
+ end
102
+
103
+ describe "#get" do
104
+ before(:each) do
105
+ @id = random_integer
106
+ @entity = DummyEntity.new(random_string)
107
+ DummyEntity.stub(:new).and_return(@ride)
108
+ @events = [mock("Event", :apply => true), mock("Event", :apply => true)]
109
+
110
+ @storage_client = mock("StorageClient", :get_entity => @entity, :get_events => @events)
111
+ @store = Store.new
112
+ @store.stub(:storage_client) { @storage_client }
113
+ end
114
+
115
+ subject { @store.get(@id) }
116
+
117
+ it "should retrieve object from the storage client" do
118
+ @storage_client.should_receive(:get_entity).with(@id, false)
119
+ subject
120
+ end
121
+ it "should retrieve the events for the entity" do
122
+ @storage_client.should_receive(:get_events).with(@id)
123
+ subject
124
+ end
125
+ it "should apply each event" do
126
+ @events.each do |e|
127
+ e.should_receive(:apply).with(@entity)
128
+ end
129
+ subject
130
+ end
131
+ it "should return a ride" do
132
+ subject.should eq(@entity)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe EntityStore do
4
+ describe ".setup" do
5
+ before(:each) do
6
+ EntityStore.setup do |config|
7
+ config.log_level = Logger::WARN
8
+ end
9
+ end
10
+ it "has a log_level of WARN" do
11
+ EntityStore.log_level.should eq(Logger::WARN)
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,25 @@
1
+ require 'rake'
2
+ require 'rspec'
3
+ require "#{Rake.application.original_dir}/lib/entity_store"
4
+
5
+ RSpec.configure do |config|
6
+ config.color_enabled = true
7
+ end
8
+
9
+ include EntityStore
10
+
11
+ def random_string
12
+ (0...24).map{ ('a'..'z').to_a[rand(26)] }.join
13
+ end
14
+
15
+ def random_integer
16
+ rand(9999)
17
+ end
18
+
19
+ def random_time
20
+ Time.now - random_integer
21
+ end
22
+
23
+ def random_object_id
24
+ BSON::ObjectId.from_time(random_time).to_s
25
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: entity_store
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Adam Bird
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongo
16
+ requirement: &70160207835100 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.6'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70160207835100
25
+ - !ruby/object:Gem::Dependency
26
+ name: bson_ext
27
+ requirement: &70160207834240 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '1.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70160207834240
36
+ description: Event sourced entity store with a Mongo body
37
+ email: adam.bird@gmail.com
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - lib/entity_store/config.rb
43
+ - lib/entity_store/entity.rb
44
+ - lib/entity_store/entity_value.rb
45
+ - lib/entity_store/event.rb
46
+ - lib/entity_store/event_bus.rb
47
+ - lib/entity_store/event_data_object.rb
48
+ - lib/entity_store/external_store.rb
49
+ - lib/entity_store/mongo_entity_store.rb
50
+ - lib/entity_store/not_found.rb
51
+ - lib/entity_store/store.rb
52
+ - lib/entity_store/version.rb
53
+ - lib/entity_store.rb
54
+ - lib/tasks/entity_store.rake
55
+ - spec/entity_store/entity_value_spec.rb
56
+ - spec/entity_store/event_bus_spec.rb
57
+ - spec/entity_store/event_spec.rb
58
+ - spec/entity_store/external_store_spec.rb
59
+ - spec/entity_store/mongo_entity_store_spec.rb
60
+ - spec/entity_store/store_spec.rb
61
+ - spec/entity_store_spec.rb
62
+ - spec/spec_helper.rb
63
+ homepage: http://github.com/adambird/entity_store
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.10
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Event sourced entity store with a Mongo body
87
+ test_files:
88
+ - spec/entity_store/entity_value_spec.rb
89
+ - spec/entity_store/event_bus_spec.rb
90
+ - spec/entity_store/event_spec.rb
91
+ - spec/entity_store/external_store_spec.rb
92
+ - spec/entity_store/mongo_entity_store_spec.rb
93
+ - spec/entity_store/store_spec.rb
94
+ - spec/entity_store_spec.rb
95
+ - spec/spec_helper.rb