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