entity_store 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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