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