ormivore 0.0.1
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/Gemfile +14 -0
- data/Gemfile.lock +77 -0
- data/Guardfile +17 -0
- data/README.md +135 -0
- data/Rakefile +11 -0
- data/app/adapters/account_storage_ar_adapter.rb +16 -0
- data/app/adapters/account_storage_memory_adapter.rb +7 -0
- data/app/adapters/address_storage_ar_adapter.rb +8 -0
- data/app/adapters/address_storage_memory_adapter.rb +7 -0
- data/app/connection_manager.rb +17 -0
- data/app/console.rb +15 -0
- data/app/converters/account_sql_storage_converter.rb +33 -0
- data/app/converters/address_sql_storage_converter.rb +51 -0
- data/app/converters/noop_converter.rb +15 -0
- data/app/entities/account.rb +20 -0
- data/app/entities/address.rb +24 -0
- data/app/ports/account_storage_port.rb +5 -0
- data/app/ports/address_storage_port.rb +5 -0
- data/app/repos/account_repo.rb +7 -0
- data/app/repos/address_repo.rb +7 -0
- data/app/require_helpers.rb +34 -0
- data/db/database.yml +7 -0
- data/lib/console.rb +13 -0
- data/lib/init.rb +9 -0
- data/lib/ormivore/ar_adapter.rb +111 -0
- data/lib/ormivore/entity.rb +199 -0
- data/lib/ormivore/errors.rb +21 -0
- data/lib/ormivore/memory_adapter.rb +99 -0
- data/lib/ormivore/port.rb +95 -0
- data/lib/ormivore/repo.rb +58 -0
- data/lib/ormivore/version.rb +3 -0
- data/spec/adapters/account_storage_ar_adapter_spec.rb +13 -0
- data/spec/adapters/account_storage_memory_adapter_spec.rb +12 -0
- data/spec/adapters/address_storage_ar_adapter_spec.rb +14 -0
- data/spec/adapters/address_storage_memory_adapter_spec.rb +13 -0
- data/spec/adapters/ar_helpers.rb +9 -0
- data/spec/adapters/memory_helpers.rb +5 -0
- data/spec/adapters/shared.rb +146 -0
- data/spec/adapters/shared_account.rb +15 -0
- data/spec/adapters/shared_address.rb +21 -0
- data/spec/converters/account_sql_storage_converter_spec.rb +28 -0
- data/spec/converters/address_sql_storage_converter_spec.rb +48 -0
- data/spec/entities/account_spec.rb +13 -0
- data/spec/entities/address_spec.rb +17 -0
- data/spec/entities/shared.rb +114 -0
- data/spec/factories.rb +18 -0
- data/spec/integration/account_repo_ar_integration_spec.rb +12 -0
- data/spec/integration/account_repo_memory_integration_spec.rb +11 -0
- data/spec/integration/address_repo_ar_integration_spec.rb +13 -0
- data/spec/integration/address_repo_memory_integration_spec.rb +13 -0
- data/spec/integration/shared.rb +74 -0
- data/spec/integration/shared_account.rb +17 -0
- data/spec/integration/shared_address.rb +23 -0
- data/spec/ports/account_storage_port_spec.rb +6 -0
- data/spec/ports/address_storage_port_spec.rb +6 -0
- data/spec/ports/shared.rb +50 -0
- data/spec/repos/account_repo_spec.rb +6 -0
- data/spec/repos/address_repo_spec.rb +6 -0
- data/spec/repos/shared.rb +92 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_db_helper.rb +54 -0
- data/spec/spec_helper.rb +8 -0
- metadata +187 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'nested_exceptions'
|
2
|
+
|
3
|
+
module ORMivore
|
4
|
+
class NotImplementedYet < StandardError; end
|
5
|
+
|
6
|
+
class ORMivoreError < StandardError
|
7
|
+
include NestedExceptions
|
8
|
+
end
|
9
|
+
|
10
|
+
class BadArgumentError < ORMivoreError; end
|
11
|
+
class BadAttributesError < BadArgumentError; end
|
12
|
+
class BadConditionsError < BadArgumentError; end
|
13
|
+
|
14
|
+
class InvalidStateError < ORMivoreError; end
|
15
|
+
|
16
|
+
class AbstractMethodError < ORMivoreError; end
|
17
|
+
|
18
|
+
class StorageError < ORMivoreError; end
|
19
|
+
class RecordNotFound < StorageError; end
|
20
|
+
class RecordAlreadyExists < StorageError; end
|
21
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module ORMivore
|
2
|
+
module MemoryAdapter
|
3
|
+
module ClassMethods
|
4
|
+
attr_reader :default_converter_class
|
5
|
+
|
6
|
+
private
|
7
|
+
attr_writer :default_converter_class
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(converter = nil)
|
15
|
+
@converter = converter || self.class.default_converter_class.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def find(conditions, attributes_to_load, options = {})
|
19
|
+
order = options.fetch(:order, {})
|
20
|
+
|
21
|
+
reorder(
|
22
|
+
filter_from_storage(conditions, attributes_to_load),
|
23
|
+
order
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create(attrs)
|
28
|
+
id = attrs[:id]
|
29
|
+
if id
|
30
|
+
raise RecordAlreadyExists if storage.any? { |o| o[:id] == id }
|
31
|
+
else
|
32
|
+
id = next_id
|
33
|
+
end
|
34
|
+
attrs.merge(id: id).tap { |attrs_with_id|
|
35
|
+
storage << attrs_with_id
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def update(attrs, conditions)
|
40
|
+
select_from_storage(conditions).each { |record|
|
41
|
+
record.merge!(attrs)
|
42
|
+
}.length
|
43
|
+
end
|
44
|
+
|
45
|
+
# open for tests, not to be used by any other code
|
46
|
+
def storage
|
47
|
+
@storage ||= []
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def select_from_storage(conditions)
|
53
|
+
storage.select { |o|
|
54
|
+
conditions.all? { |k, v|
|
55
|
+
if v.is_a?(Enumerable)
|
56
|
+
v.include?(o[k])
|
57
|
+
else
|
58
|
+
o[k] == v
|
59
|
+
end
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def filter_from_storage(conditions, attributes_to_load)
|
65
|
+
select_from_storage(conditions).map { |record|
|
66
|
+
record.select { |k, v| attributes_to_load.include?(k) }
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def reorder(records, order)
|
71
|
+
return records if order.empty?
|
72
|
+
|
73
|
+
records.sort { |x, y|
|
74
|
+
order.inject(0) { |acc, (k, v)|
|
75
|
+
break unless acc.zero?
|
76
|
+
|
77
|
+
multiplier =
|
78
|
+
case v
|
79
|
+
when :ascending
|
80
|
+
1
|
81
|
+
when :descending
|
82
|
+
-1
|
83
|
+
else
|
84
|
+
raise BadArgumentError, "Order direction #{v} is invalid"
|
85
|
+
end
|
86
|
+
(x[k] <=> y[k]) * multiplier
|
87
|
+
}
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :converter
|
92
|
+
|
93
|
+
def next_id
|
94
|
+
(@next_id ||= 1).tap { |current_id|
|
95
|
+
@next_id = current_id + 1
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# TODO maybe add validations for conditions, if not attributes
|
2
|
+
# TODO add support for transactions
|
3
|
+
module ORMivore
|
4
|
+
module Port
|
5
|
+
module ClassMethods
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
private
|
9
|
+
attr_writer :attributes
|
10
|
+
|
11
|
+
=begin
|
12
|
+
def finders(*methods)
|
13
|
+
methods.each do |method|
|
14
|
+
instance_eval <<-EOS
|
15
|
+
def #{method}(*args, &block)
|
16
|
+
storage.__send__(:#{method}, *args, &block)
|
17
|
+
end
|
18
|
+
EOS
|
19
|
+
end
|
20
|
+
end
|
21
|
+
=end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(base)
|
25
|
+
base.extend(ClassMethods)
|
26
|
+
end
|
27
|
+
# a good place to add generic storage functionality,
|
28
|
+
# like 'around' logging/performance monitoring/notifications/etc
|
29
|
+
# first obvious candidate is exception handling
|
30
|
+
|
31
|
+
def initialize(adapter)
|
32
|
+
@adapter = adapter
|
33
|
+
end
|
34
|
+
|
35
|
+
def find(conditions, attributes_to_load, options = {})
|
36
|
+
# TODO verify conditions to contain only keys that match attribute names and value of proper type
|
37
|
+
validate_finder_options(options, attributes_to_load)
|
38
|
+
|
39
|
+
adapter.find(conditions, attributes_to_load, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def create(attrs)
|
43
|
+
begin
|
44
|
+
adapter.create(attrs)
|
45
|
+
rescue => e
|
46
|
+
raise ORMivore::StorageError, e.message
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def update(attrs, conditions)
|
51
|
+
adapter.update(attrs, conditions)
|
52
|
+
rescue => e
|
53
|
+
raise ORMivore::StorageError, e.message
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :adapter
|
59
|
+
|
60
|
+
=begin
|
61
|
+
def attributes
|
62
|
+
self.class.attributes
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_conditions(conditions)
|
66
|
+
extra = conditions.keys - attributes.keys
|
67
|
+
raise BadConditionsError, extra.join("\n") unless extra.empty?
|
68
|
+
end
|
69
|
+
=end
|
70
|
+
|
71
|
+
def validate_finder_options(options, attributes_to_load)
|
72
|
+
options = options.dup
|
73
|
+
valid = true
|
74
|
+
|
75
|
+
# TODO how about other finder options, like limit and offset?
|
76
|
+
order = options.delete(:order) || {}
|
77
|
+
valid = false unless options.empty?
|
78
|
+
|
79
|
+
raise ORMivore::BadArgumentError, "Invalid finder options #{options.inspect}" unless valid
|
80
|
+
|
81
|
+
validate_order(order, attributes_to_load)
|
82
|
+
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate_order(order, attributes_to_load)
|
87
|
+
# TODO matching agains attributes_to_load is not good, sometimes user wants to sort on non-loaded attribute
|
88
|
+
return if order.empty?
|
89
|
+
|
90
|
+
unless order.keys.all? { |k| attributes_to_load.include?(k) }
|
91
|
+
raise BadArgumentError, "Invalid order key in #{order.inspect}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ORMivore
|
2
|
+
module Repo
|
3
|
+
module ClassMethods
|
4
|
+
attr_reader :default_entity_class
|
5
|
+
|
6
|
+
private
|
7
|
+
attr_writer :default_entity_class
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(port, entity_class = nil)
|
15
|
+
@port = port
|
16
|
+
@entity_class = entity_class || self.class.default_entity_class
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_by_id(id, options = {})
|
20
|
+
quiet = options.fetch(:quiet, false)
|
21
|
+
|
22
|
+
attrs_to_entity(port.find(
|
23
|
+
{ id: id },
|
24
|
+
[:id].concat(entity_class.attributes_list),
|
25
|
+
{}
|
26
|
+
).first
|
27
|
+
).tap { |record|
|
28
|
+
raise RecordNotFound, "#{entity_class.name} with id #{id} was not found" if record.nil? && !quiet
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def persist(entity)
|
33
|
+
if entity.id
|
34
|
+
count = port.update(entity.changes, { :id => entity.id })
|
35
|
+
raise ORMivore::StorageError, 'No records updated' if count.zero?
|
36
|
+
raise ORMivore::StorageError, 'WTF' if count > 1
|
37
|
+
|
38
|
+
entity_class.construct(entity.attributes, entity.id)
|
39
|
+
else
|
40
|
+
attrs_to_entity(port.create(entity.changes))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :port, :entity_class
|
47
|
+
|
48
|
+
def attrs_to_entity(attrs)
|
49
|
+
if attrs
|
50
|
+
entity_id = attrs.delete(:id)
|
51
|
+
attrs.reject! {|k,v| v.nil? }
|
52
|
+
entity_class.construct(attrs, entity_id)
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative 'shared_account'
|
4
|
+
require_relative 'ar_helpers'
|
5
|
+
|
6
|
+
describe App::AccountStorageArAdapter, :relational_db do
|
7
|
+
include ArHelpers
|
8
|
+
|
9
|
+
let(:entity_table) { 'accounts' }
|
10
|
+
let(:adapter) { App::AccountStorageArAdapter.new }
|
11
|
+
|
12
|
+
it_behaves_like 'an account adapter'
|
13
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative 'shared_account'
|
4
|
+
require_relative 'memory_helpers'
|
5
|
+
|
6
|
+
describe App::AccountStorageMemoryAdapter do
|
7
|
+
include MemoryHelpers
|
8
|
+
|
9
|
+
let(:adapter) { App::AccountStorageMemoryAdapter.new }
|
10
|
+
|
11
|
+
it_behaves_like 'an account adapter'
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative 'shared_address'
|
4
|
+
require_relative 'ar_helpers'
|
5
|
+
|
6
|
+
describe App::AddressStorageArAdapter, :relational_db do
|
7
|
+
include ArHelpers
|
8
|
+
|
9
|
+
let(:account_adapter) { App::AccountStorageArAdapter.new }
|
10
|
+
let(:entity_table) { 'addresses' }
|
11
|
+
let(:adapter) { App::AddressStorageArAdapter.new }
|
12
|
+
|
13
|
+
it_behaves_like 'an address adapter'
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative 'shared_address'
|
4
|
+
require_relative 'memory_helpers'
|
5
|
+
|
6
|
+
describe App::AddressStorageMemoryAdapter do
|
7
|
+
include MemoryHelpers
|
8
|
+
|
9
|
+
let(:account_adapter) { App::AccountStorageMemoryAdapter.new }
|
10
|
+
let(:adapter) { App::AddressStorageMemoryAdapter.new }
|
11
|
+
|
12
|
+
it_behaves_like 'an address adapter'
|
13
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
shared_examples_for 'an adapter' do
|
2
|
+
let(:test_value) { 'Foo' }
|
3
|
+
|
4
|
+
let(:attrs_list) { [:id].concat(attrs.keys) }
|
5
|
+
|
6
|
+
subject { adapter } # only 1 instance of adapter, please
|
7
|
+
|
8
|
+
def create_entity(overrides = {})
|
9
|
+
FactoryGirl.create(
|
10
|
+
factory_name, factory_attrs.merge(adapter: subject, test_attr => test_value).merge(overrides)
|
11
|
+
).attributes.symbolize_keys
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'responds to find' do
|
15
|
+
subject.should respond_to(:find)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#find' do
|
19
|
+
context 'when conditions are empty' do
|
20
|
+
it 'returns all available records' do
|
21
|
+
subject.find({}, attrs_list).should be_empty
|
22
|
+
create_entity
|
23
|
+
subject.find({}, attrs_list).should have(1).record
|
24
|
+
create_entity
|
25
|
+
subject.find({}, attrs_list).should have(2).records
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when conditions points to non-existing entity' do
|
30
|
+
it 'returns empty array' do
|
31
|
+
subject.find({id: 123456789}, attrs_list).should be_empty
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when conditions point to existing entity' do
|
36
|
+
it 'returns entity id' do
|
37
|
+
entity = create_entity
|
38
|
+
data = subject.find({id: entity[:id]}, attrs_list)
|
39
|
+
data.first[:id].should == entity[:id]
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'returns proper entity attrs' do
|
43
|
+
entity = create_entity
|
44
|
+
data = subject.find({id: entity[:id]}, attrs_list)
|
45
|
+
data.should_not be_nil
|
46
|
+
data.first[test_attr].should == entity[test_attr]
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns only required entity attrs' do
|
50
|
+
entity = create_entity
|
51
|
+
data = subject.find({id: entity[:id]}, [test_attr]).first
|
52
|
+
data.should == { test_attr => entity[test_attr] }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when conditions point to multiple entities' do
|
57
|
+
before do
|
58
|
+
create_entity(test_attr => 'v1')
|
59
|
+
create_entity(test_attr => 'v2')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns array of attributes' do
|
63
|
+
subject.find({}, attrs_list).should have(2).records
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when ordering criteria is provided' do
|
67
|
+
it 'sorts records in ascending order' do
|
68
|
+
subject.find(
|
69
|
+
{}, attrs_list, order: { test_attr => :ascending }
|
70
|
+
).map { |o| o[test_attr] }.should == ['v1', 'v2']
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'sorts records in descending order' do
|
74
|
+
subject.find(
|
75
|
+
{}, attrs_list, order: { test_attr => :descending }
|
76
|
+
).map { |o| o[test_attr] }.should == ['v2', 'v1']
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'raises error if unknown ordering is provided' do
|
80
|
+
expect {
|
81
|
+
subject.find({}, attrs_list, order: { test_attr => :foo })
|
82
|
+
}.to raise_error ORMivore::BadArgumentError
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#create' do
|
89
|
+
context 'when attempting to create record with id that is already present in database' do
|
90
|
+
it 'raises error' do
|
91
|
+
expect {
|
92
|
+
subject.create(subject.create(attrs))
|
93
|
+
}.to raise_error ORMivore::StorageError
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'when record does not have an id' do
|
98
|
+
it 'returns back attributes including new id' do
|
99
|
+
data = subject.create(attrs)
|
100
|
+
data.should include(attrs)
|
101
|
+
data[:id].should be_kind_of(Integer)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'inserts record in database' do
|
105
|
+
data = subject.create(attrs)
|
106
|
+
|
107
|
+
load_test_value(data[:id]).should == test_value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '#update' do
|
113
|
+
context 'when record did not exist' do
|
114
|
+
it 'returns 0 update count' do
|
115
|
+
create_entity
|
116
|
+
subject.update(attrs, id: 123).should == 0
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'when record existed' do
|
121
|
+
it 'returns update count 1' do
|
122
|
+
entity = create_entity
|
123
|
+
|
124
|
+
subject.update(attrs, id: entity[:id]).should == 1
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'updates record attributes' do
|
128
|
+
entity = create_entity
|
129
|
+
|
130
|
+
subject.update({test_attr => 'Bar'}, id: entity[:id])
|
131
|
+
|
132
|
+
load_test_value(entity[:id]).should == 'Bar'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'when 2 matching records existed' do
|
137
|
+
it 'returns update count 2' do
|
138
|
+
entity_ids = []
|
139
|
+
entity_ids << create_entity[:id]
|
140
|
+
entity_ids << create_entity[:id]
|
141
|
+
|
142
|
+
subject.update(attrs, id: entity_ids).should == 2
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|