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