entity_store 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/entity_store.rb +61 -0
- data/lib/entity_store/config.rb +0 -0
- data/lib/entity_store/entity.rb +31 -0
- data/lib/entity_store/entity_value.rb +36 -0
- data/lib/entity_store/event.rb +56 -0
- data/lib/entity_store/event_bus.rb +34 -0
- data/lib/entity_store/event_data_object.rb +25 -0
- data/lib/entity_store/external_store.rb +44 -0
- data/lib/entity_store/mongo_entity_store.rb +86 -0
- data/lib/entity_store/not_found.rb +7 -0
- data/lib/entity_store/store.rb +56 -0
- data/lib/entity_store/version.rb +3 -0
- data/lib/tasks/entity_store.rake +7 -0
- data/spec/entity_store/entity_value_spec.rb +77 -0
- data/spec/entity_store/event_bus_spec.rb +57 -0
- data/spec/entity_store/event_spec.rb +109 -0
- data/spec/entity_store/external_store_spec.rb +97 -0
- data/spec/entity_store/mongo_entity_store_spec.rb +46 -0
- data/spec/entity_store/store_spec.rb +135 -0
- data/spec/entity_store_spec.rb +15 -0
- data/spec/spec_helper.rb +25 -0
- metadata +95 -0
data/lib/entity_store.rb
ADDED
@@ -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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|