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,20 @@
1
+ module App
2
+ class Account
3
+ include ORMivore::Entity
4
+
5
+ STATUSES = %w(active inactive deleted).map(&:to_sym).freeze
6
+
7
+ attributes(
8
+ firstname: String,
9
+ lastname: String,
10
+ email: String,
11
+ status: Symbol
12
+ )
13
+
14
+ private
15
+
16
+ def validate
17
+ raise "Invalid status #{status}" unless STATUSES.include?(status)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module App
2
+ class Address
3
+ include ORMivore::Entity
4
+
5
+ attributes(
6
+ street_1: String,
7
+ street_2: String,
8
+ city: String,
9
+ postal_code: String,
10
+ country_code: String,
11
+ region_code: String,
12
+ type: Symbol,
13
+ account_id: Integer
14
+ )
15
+
16
+ optional :street_2, :region_code, :account_id
17
+
18
+ private
19
+
20
+ def validate
21
+ raise "Invalid type #{type}" unless %w(shipping billing).include?(type.to_s)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module App
2
+ class AccountStoragePort
3
+ include ORMivore::Port
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module App
2
+ class AddressStoragePort
3
+ include ORMivore::Port
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module App
2
+ class AccountRepo
3
+ include ORMivore::Repo
4
+
5
+ self.default_entity_class = App::Account
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module App
2
+ class AddressRepo
3
+ include ORMivore::Repo
4
+
5
+ self.default_entity_class = App::Address
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ module RequireHelpers
2
+ class << self
3
+ def root
4
+ File.expand_path('../..', __FILE__)
5
+ end
6
+
7
+ def augment_load_path
8
+ $LOAD_PATH.unshift(File.join(root, 'lib'))
9
+ $LOAD_PATH.unshift(File.join(root, 'app'))
10
+ $LOAD_PATH.unshift(root)
11
+ end
12
+
13
+ def require_independent_files_in_dir(dir)
14
+ Dir.glob(File.join(root, dir, '*.rb')).each do |absolute_path|
15
+ short_path = absolute_path.sub(/^#{root}\/lib\/(.*)\.rb$/, '\1')
16
+ require short_path
17
+ end
18
+ end
19
+
20
+ def require_all
21
+ augment_load_path
22
+
23
+ require 'init'
24
+
25
+ require 'app/connection_manager'
26
+
27
+ require_independent_files_in_dir 'app/converters'
28
+ require_independent_files_in_dir 'app/adapters'
29
+ require_independent_files_in_dir 'app/ports'
30
+ require_independent_files_in_dir 'app/entities'
31
+ require_independent_files_in_dir 'app/repos'
32
+ end
33
+ end
34
+ end
data/db/database.yml ADDED
@@ -0,0 +1,7 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/db.test.sqlite3
4
+
5
+ dev:
6
+ adapter: sqlite3
7
+ database: db/db.dev.sqlite3
data/lib/console.rb ADDED
@@ -0,0 +1,13 @@
1
+ # Allows running a lightweight version of rails console
2
+ # Start it like this:
3
+ #
4
+ # irb -r ./lib/console.rb
5
+
6
+ require 'rubygems'
7
+ require 'bundler/setup'
8
+
9
+ Bundler.require(:sinatra)
10
+
11
+ require_relative 'app'
12
+
13
+ MobileCheckout::ConnectionManager.establish_connection :development
data/lib/init.rb ADDED
@@ -0,0 +1,9 @@
1
+ #root = File.expand_path('..', __FILE__)
2
+ #$LOAD_PATH.unshift(File.join(root))
3
+
4
+ require 'ormivore/errors'
5
+ require 'ormivore/entity'
6
+ require 'ormivore/port'
7
+ require 'ormivore/repo'
8
+ require 'ormivore/ar_adapter'
9
+ require 'ormivore/memory_adapter'
@@ -0,0 +1,111 @@
1
+ # TODO ArAdapter is really ugly; replace it with some simple Sql adapter without AR 'goodness'
2
+ module ORMivore
3
+ module ArAdapter
4
+ module ClassMethods
5
+ attr_reader :default_converter_class
6
+ attr_reader :table_name
7
+
8
+ def ar_class
9
+ finalize
10
+ self::ArRecord
11
+ end
12
+
13
+ private
14
+ attr_writer :default_converter_class
15
+ attr_writer :table_name
16
+
17
+ def expand_on_create(&block)
18
+ @expand_on_create = block
19
+ end
20
+
21
+ def finalize
22
+ unless @finalized
23
+ @finalized = true
24
+
25
+ file, line = caller.first.split(':', 2)
26
+ line = line.to_i
27
+
28
+ module_eval(<<-EOS, file, line - 1)
29
+ class ArRecord < ActiveRecord::Base
30
+ self.table_name = '#{table_name}'
31
+ self.inheritance_column = :_type_disabled
32
+ def attributes_protected_by_default; []; end
33
+ end
34
+ EOS
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.included(base)
40
+ base.extend(ClassMethods)
41
+ end
42
+
43
+ def initialize(converter = nil)
44
+ @converter = converter || self.class.default_converter_class.new
45
+ end
46
+
47
+ def find(conditions, attributes_to_load, options = {})
48
+ order = options.fetch(:order, {})
49
+
50
+ ar_class.all(
51
+ select: converter.attributes_list_to_storage(attributes_to_load),
52
+ conditions: conditions,
53
+ order: order_by_clause(order)
54
+ ).map { |r| entity_attributes(r) }
55
+ end
56
+
57
+ def create(attrs)
58
+ record = ar_class.create!(
59
+ extend_with_defaults(
60
+ converter.to_storage(attrs))) { |o| o.id = attrs[:id] }
61
+ attrs.merge(id: record.id)
62
+ rescue ActiveRecord::ActiveRecordError => e
63
+ raise StorageError.new(e)
64
+ end
65
+
66
+ def update(attrs, conditions)
67
+ ar_class.update_all(converter.to_storage(attrs), conditions)
68
+ rescue ActiveRecord::ActiveRecordError => e
69
+ raise StorageError.new(e)
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :converter
75
+
76
+ def extend_with_defaults(attrs)
77
+ expansion = self.class.instance_variable_get(:@expand_on_create)
78
+ if expansion
79
+ attrs.merge(expansion.call(attrs))
80
+ else
81
+ attrs
82
+ end
83
+ end
84
+
85
+ def order_by_clause(order)
86
+ return '' if order.empty?
87
+
88
+ order.map { |k, v|
89
+ direction =
90
+ case v
91
+ when :ascending
92
+ 'asc'
93
+ when :descending
94
+ 'desc'
95
+ else
96
+ raise BadArgumentError, "Order direction #{v} is invalid"
97
+ end
98
+
99
+ "#{k} #{direction}"
100
+ }.join(', ')
101
+ end
102
+
103
+ def ar_class
104
+ self.class.ar_class
105
+ end
106
+
107
+ def entity_attributes(record)
108
+ converter.from_storage(record.attributes.symbolize_keys)
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,199 @@
1
+ # TODO figure out how to add validation in a nice way
2
+ module ORMivore
3
+ module Entity
4
+ module ClassMethods
5
+ ALLOWED_ATTRIBUTE_TYPES = [String, Symbol, Integer, Float].freeze
6
+
7
+ attr_reader :attributes_declaration
8
+ attr_reader :optional_attributes_list
9
+
10
+ def attributes_list
11
+ attributes_declaration.keys
12
+ end
13
+
14
+ def construct(attrs, id)
15
+ id = coerce_id(id)
16
+
17
+ coerced_attrs = attrs.symbolize_keys.tap { |h| coerce(h) }.freeze
18
+
19
+ base_attributes = coerced_attrs
20
+ dirty_attributes = {}.freeze
21
+
22
+ validate_presence_of_proper_attributes(base_attributes, dirty_attributes)
23
+
24
+ obj = allocate
25
+
26
+ obj.instance_variable_set(:@id, id)
27
+ obj.instance_variable_set(:@base_attributes, base_attributes)
28
+ obj.instance_variable_set(:@dirty_attributes, dirty_attributes)
29
+
30
+ # TODO how to do custom validation?
31
+ # validate
32
+
33
+ obj
34
+ end
35
+
36
+ def validate_presence_of_proper_attributes(base, dirty)
37
+ # doing complicated way first because it is much more memory efficient
38
+ # but it does not allow for good error messages, so if something is
39
+ # wrong, need to proceed to inefficient validation that produces nice
40
+ # messages
41
+ missing = 0
42
+ known_counts = attributes_list.each_with_object([0, 0]) { |attr, acc|
43
+ acc[0] += 1 if base[attr]
44
+ acc[1] += 1 if dirty[attr]
45
+ missing +=1 unless optional_attributes_list.include?(attr) || base[attr] || dirty[attr]
46
+ }
47
+
48
+ if missing > 0 || [base.length, dirty.length] != known_counts
49
+ expensive_validate_presence_of_proper_attributes(
50
+ base.merge(dirty)
51
+ )
52
+ end
53
+ end
54
+
55
+ def coerce(attrs)
56
+ attrs.each do |name, type|
57
+ attr_value = attrs[name]
58
+ declared_type = attributes_declaration[name]
59
+ if declared_type && !attr_value.is_a?(declared_type)
60
+ attrs[name] = Kernel.public_send(declared_type.name.to_sym, attr_value)
61
+ end
62
+ end
63
+ rescue ArgumentError => e
64
+ raise ORMivore::BadArgumentError.new(e)
65
+ end
66
+
67
+ private
68
+
69
+ def attributes(declaration)
70
+ @attributes_declaration = declaration.symbolize_keys.freeze
71
+ validate_attributes_declaration
72
+ # @attributes_list = methods.map(&:to_sym)
73
+ @optional_attributes_list ||= []
74
+
75
+ attributes_list.map(&:to_s).each do |attr|
76
+ module_eval(<<-EOS)
77
+ def #{attr}
78
+ @dirty_attributes[:#{attr}] || @base_attributes[:#{attr}]
79
+ end
80
+ EOS
81
+ self::Builder.module_eval(<<-EOS)
82
+ def #{attr}
83
+ attributes[:#{attr}]
84
+ end
85
+ def #{attr}=(value)
86
+ attributes[:#{attr}] = value
87
+ end
88
+ EOS
89
+ end
90
+ end
91
+
92
+ def optional(*methods)
93
+ @optional_attributes_list = methods.map(&:to_sym)
94
+ end
95
+
96
+ # now, really private methods, not part of API
97
+
98
+ # TODO figure out how to differenciate private methods that are part of
99
+ # ORMivore API from those that are NOT
100
+ def expensive_validate_presence_of_proper_attributes(attrs)
101
+ attributes_list.each do |attr|
102
+ unless attrs.delete(attr) || optional_attributes_list.include?(attr)
103
+ raise BadAttributesError, "Missing attribute '#{attr}'"
104
+ end
105
+ end
106
+
107
+ raise BadAttributesError, "Unknown attributes #{attrs.inspect}" unless attrs.empty?
108
+ end
109
+
110
+ def validate_attributes_declaration
111
+ attributes_declaration.each do |name, type|
112
+ unless ALLOWED_ATTRIBUTE_TYPES.include?(type)
113
+ raise ORMivore::BadArgumentError, "Invalid attribute type #{type.inspect}"
114
+ end
115
+ end
116
+ end
117
+
118
+ def coerce_id(value)
119
+ value ? Integer(value) : nil
120
+ rescue ArgumentError
121
+ raise ORMivore::BadArgumentError, "Not a valid id: #{value.inspect}"
122
+ end
123
+ end
124
+
125
+ def self.included(base)
126
+ base.extend(ClassMethods)
127
+
128
+ base.module_eval(<<-EOS)
129
+ class Builder
130
+ def initialize
131
+ @attributes = {}
132
+ end
133
+
134
+ def id
135
+ attributes[:id]
136
+ end
137
+
138
+ def adapter=(value)
139
+ @adapter = value
140
+ end
141
+
142
+ # FactoryGirl integration point
143
+ def save!
144
+ @attributes = @adapter.create(attributes)
145
+ end
146
+
147
+ attr_reader :attributes
148
+ end
149
+ EOS
150
+ end
151
+
152
+ attr_reader :id
153
+
154
+ def attributes
155
+ all_attributes
156
+ end
157
+
158
+ def changes
159
+ @dirty_attributes
160
+ end
161
+
162
+ def apply(attrs)
163
+ self.dup.tap { |other|
164
+ other.expand_changes(attrs)
165
+ }
166
+ end
167
+
168
+ protected
169
+
170
+ # to be used only by #change
171
+ def expand_changes(attrs)
172
+ attrs = attrs.symbolize_keys.tap { |h| self.class.coerce(h) }
173
+ @dirty_attributes = @dirty_attributes.merge(attrs).freeze # melt and freeze, huh
174
+ @all_attributes = nil # it is not valid anymore
175
+
176
+ self.class.validate_presence_of_proper_attributes(@base_attributes, @dirty_attributes)
177
+ end
178
+
179
+ private
180
+
181
+ def all_attributes
182
+ # memory / performance tradeoff can be played with here by keeping
183
+ # all_attributes around or generating it each time
184
+ @all_attributes = @base_attributes.merge(@dirty_attributes)
185
+ end
186
+
187
+ def initialize(attrs)
188
+ coerced_attrs = attrs.symbolize_keys.tap { |h| self.class.coerce(h) }.freeze
189
+
190
+ @base_attributes = {}.freeze
191
+ @dirty_attributes = coerced_attrs
192
+
193
+ self.class.validate_presence_of_proper_attributes(@base_attributes, @dirty_attributes)
194
+
195
+ # TODO how to do custom validation?
196
+ # validate
197
+ end
198
+ end
199
+ end