ormivore 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/Gemfile +14 -0
  2. data/Gemfile.lock +77 -0
  3. data/Guardfile +17 -0
  4. data/README.md +135 -0
  5. data/Rakefile +11 -0
  6. data/app/adapters/account_storage_ar_adapter.rb +16 -0
  7. data/app/adapters/account_storage_memory_adapter.rb +7 -0
  8. data/app/adapters/address_storage_ar_adapter.rb +8 -0
  9. data/app/adapters/address_storage_memory_adapter.rb +7 -0
  10. data/app/connection_manager.rb +17 -0
  11. data/app/console.rb +15 -0
  12. data/app/converters/account_sql_storage_converter.rb +33 -0
  13. data/app/converters/address_sql_storage_converter.rb +51 -0
  14. data/app/converters/noop_converter.rb +15 -0
  15. data/app/entities/account.rb +20 -0
  16. data/app/entities/address.rb +24 -0
  17. data/app/ports/account_storage_port.rb +5 -0
  18. data/app/ports/address_storage_port.rb +5 -0
  19. data/app/repos/account_repo.rb +7 -0
  20. data/app/repos/address_repo.rb +7 -0
  21. data/app/require_helpers.rb +34 -0
  22. data/db/database.yml +7 -0
  23. data/lib/console.rb +13 -0
  24. data/lib/init.rb +9 -0
  25. data/lib/ormivore/ar_adapter.rb +111 -0
  26. data/lib/ormivore/entity.rb +199 -0
  27. data/lib/ormivore/errors.rb +21 -0
  28. data/lib/ormivore/memory_adapter.rb +99 -0
  29. data/lib/ormivore/port.rb +95 -0
  30. data/lib/ormivore/repo.rb +58 -0
  31. data/lib/ormivore/version.rb +3 -0
  32. data/spec/adapters/account_storage_ar_adapter_spec.rb +13 -0
  33. data/spec/adapters/account_storage_memory_adapter_spec.rb +12 -0
  34. data/spec/adapters/address_storage_ar_adapter_spec.rb +14 -0
  35. data/spec/adapters/address_storage_memory_adapter_spec.rb +13 -0
  36. data/spec/adapters/ar_helpers.rb +9 -0
  37. data/spec/adapters/memory_helpers.rb +5 -0
  38. data/spec/adapters/shared.rb +146 -0
  39. data/spec/adapters/shared_account.rb +15 -0
  40. data/spec/adapters/shared_address.rb +21 -0
  41. data/spec/converters/account_sql_storage_converter_spec.rb +28 -0
  42. data/spec/converters/address_sql_storage_converter_spec.rb +48 -0
  43. data/spec/entities/account_spec.rb +13 -0
  44. data/spec/entities/address_spec.rb +17 -0
  45. data/spec/entities/shared.rb +114 -0
  46. data/spec/factories.rb +18 -0
  47. data/spec/integration/account_repo_ar_integration_spec.rb +12 -0
  48. data/spec/integration/account_repo_memory_integration_spec.rb +11 -0
  49. data/spec/integration/address_repo_ar_integration_spec.rb +13 -0
  50. data/spec/integration/address_repo_memory_integration_spec.rb +13 -0
  51. data/spec/integration/shared.rb +74 -0
  52. data/spec/integration/shared_account.rb +17 -0
  53. data/spec/integration/shared_address.rb +23 -0
  54. data/spec/ports/account_storage_port_spec.rb +6 -0
  55. data/spec/ports/address_storage_port_spec.rb +6 -0
  56. data/spec/ports/shared.rb +50 -0
  57. data/spec/repos/account_repo_spec.rb +6 -0
  58. data/spec/repos/address_repo_spec.rb +6 -0
  59. data/spec/repos/shared.rb +92 -0
  60. data/spec/spec.opts +3 -0
  61. data/spec/spec_db_helper.rb +54 -0
  62. data/spec/spec_helper.rb +8 -0
  63. 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,3 @@
1
+ module ORMivore
2
+ VERSION = "0.0.1"
3
+ 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,9 @@
1
+ module ArHelpers
2
+ def execute_simple_string_query(query)
3
+ ActiveRecord::Base.connection.execute(query).first[0]
4
+ end
5
+
6
+ def load_test_value(id)
7
+ execute_simple_string_query( "select #{test_attr.to_s} from #{entity_table} where id = #{id}")
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module MemoryHelpers
2
+ def load_test_value(id)
3
+ adapter.storage.first { |o| o[:id] = id }[test_attr]
4
+ end
5
+ 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