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