submodel 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6a7f2069e601f125d05758bcccf692a42dbb5144
4
+ data.tar.gz: eb2f1f535261d908eec8a26bb9c7c4922e4f8d0e
5
+ SHA512:
6
+ metadata.gz: 030119f82f68abd4d07d19d666ad62dd96f4c3b4fd501bb1bdaf00346e1c61d0df9a85bc47bafcd725e293d56416d550335370548ddbc3a2609d806e01248044
7
+ data.tar.gz: 31bd5140a8133d4c4a1c1a3fcf3a14944aa47bca5007127535e18d0aa68b2f64ad8c885f038561095953ea6ffe647897feebe148738ce1cd7f84b05483858c2e
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Rafaël Blais Masson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Submodel
2
+
3
+ Submodel maps ActiveRecord columns to ActiveModel models, so that [hstore](http://www.postgresql.org/docs/9.3/static/hstore.html) or [serialized](http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html) hash columns can share validations and be augmented with some methods. This can greatly help cleanup your business logic.
4
+
5
+ ## Usage
6
+
7
+ ```ruby
8
+ # Gemfile
9
+ gem 'submodel'
10
+ ```
11
+
12
+ Create a submodel with `ActiveModel::Model`. Here’s an example `Address` model using [Carmen](http://github.com/jim/carmen) to provide country and state validations.
13
+
14
+ ```ruby
15
+ # app/submodels/mailing_address.rb
16
+
17
+ class Address
18
+ include ActiveModel::Model
19
+
20
+ COUNTRY_CODES = Carmen::Country.all.map(&:code)
21
+ CA_STATE_CODES = Carmen::Country.coded('CA').subregions.map(&:code)
22
+ US_STATE_CODES = Carmen::Country.coded('US').subregions.map(&:code)
23
+
24
+ attr_accessor :street_1, :street_2, :city, :state, :country, :postal_code
25
+
26
+ validates_inclusion_of :country, in: COUNTRY_CODES
27
+ validates_inclusion_of :state, in: CA_STATE_CODES, if: :canada?
28
+ validates_inclusion_of :state, in: US_STATE_CODES, if: :united_states?
29
+
30
+ def canada?
31
+ country == 'CA'
32
+ end
33
+
34
+ def united_states?
35
+ country == 'US'
36
+ end
37
+ end
38
+ ```
39
+
40
+ Use the `submodel` method to map your ActiveRecord columns to the submodel.
41
+
42
+ ```ruby
43
+ # app/models/order.rb
44
+
45
+ class Order < ActiveRecord::Base
46
+ submodel :billing_address, Address
47
+ end
48
+ ```
49
+
50
+ Then, accessing `#billing_address` will return an instance created with `Address.new`. Similarly, passing a hash to `#billing_address=` will create a new instance with the hash as argument.
51
+
52
+ ```ruby
53
+ order = Order.new
54
+ order.attributes # => { "id" => nil, "billing_address" => nil }
55
+
56
+ order.billing_address # => #<Address>
57
+ order.billing_address.blank? # => true
58
+
59
+ order.billing_address.street_1 = '123 Fake Street'
60
+ order.billing_address # => #<Address street_1="123 Fake Street">
61
+ order.billing_address.blank? # => false
62
+
63
+ order.billing_address = { country: 'CA', state: 'QC' }
64
+ order.billing_address # => #<Address state="QC" country="CA">
65
+ ```
66
+
67
+ Note: While the getter creates an instance on demand, blank submodels are persisted as `NULL`.
68
+
69
+ ## Comparison
70
+
71
+ When using `==`, your submodel columns will be compared based on the stringified hash of their instance variables. Blank variables are ignored.
72
+
73
+ ```ruby
74
+ order = Order.new
75
+ order.billing_address # => #<Address>
76
+
77
+ order.billing_address == Address.new # => true
78
+ order.billing_address == {} # => true
79
+ order.billing_address == { street_1: '', street_2: ' ' } # => true
80
+ order.billing_address == { street_1: 'foo', street_2: 'bar' } # => false
81
+
82
+ order.billing_address.country = 'CA'
83
+ order.billing_address.state = 'QC'
84
+ order.billing_address == { 'country' => 'CA', :state => 'QC' } # => true
85
+ order.billing_address == Address.new(country: 'CA') # => false
86
+ ```
87
+
88
+ ## Extending submodels per-column
89
+
90
+ You can pass the `submodel` method a block to be executed at the class level. For instance, this adds an (unfortunate) validation to `shipping_address`, leaving `billing_address` as is.
91
+
92
+ ```ruby
93
+ class Order < ActiveRecord::Base
94
+ submodel :billing_address, Address
95
+ submodel :shipping_address, Address do
96
+ validates :country, inclusion: { in: %w[US CA] }
97
+ end
98
+ end
99
+ ```
100
+
101
+ ## This gem seems overkill.
102
+
103
+ You might think “Why not just override the getter and setter?” In my experience, getting this *right* is always more complex. If you want proper behavior (validation, comparison, FormBuilder support, persistence) you basically have to repeat [this code](lib/submodel/active_record.rb) for every column.
104
+
105
+ ---
106
+
107
+ © 2014 [Rafaël Blais Masson](http://rafbm.com). Submodel is released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,102 @@
1
+ module Submodel
2
+ def self.values(object)
3
+ object.instance_values.select { |key, value|
4
+ # This filters out ActiveModel::Validation’s @error variable since it has no setter
5
+ object.respond_to?("#{key}=") && value.present?
6
+ }
7
+ end
8
+
9
+ module ActiveRecord
10
+ extend ActiveSupport::Concern
11
+
12
+ module ClassMethods
13
+ def submodel(attr, klass, validation_options = {}, &block)
14
+ column = columns_hash[attr.to_s]
15
+
16
+ augmented_klass = Class.new(klass) do
17
+ define_singleton_method :name do
18
+ klass.name
19
+ end
20
+
21
+ define_method :inspect do
22
+ attrs = Submodel.values(self).map { |k,v| "#{k}=#{v.inspect}" }.join(' ').presence
23
+ string = [klass.name, attrs].compact.join(' ')
24
+ "#<#{string}>"
25
+ end
26
+ alias_method :to_s, :inspect
27
+
28
+ define_method :blank? do
29
+ Submodel.values(self).blank?
30
+ end
31
+
32
+ define_method :== do |other|
33
+ hash = Submodel.values(self)
34
+
35
+ if other.is_a? klass
36
+ hash == Submodel.values(other)
37
+ elsif other.is_a? Hash
38
+ hash == other.stringify_keys
39
+ else
40
+ hash == other
41
+ end
42
+ end
43
+ end
44
+
45
+ if block_given?
46
+ augmented_klass.class_eval &block
47
+ end
48
+
49
+ serialize attr, Module.new {
50
+ define_singleton_method :load do |value|
51
+ if value.is_a? String
52
+ case column.type
53
+ when :hstore
54
+ value = ::ActiveRecord::ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value)
55
+ when :json
56
+ value = JSON.parse(value)
57
+ else
58
+ value = YAML.load(value)
59
+ end
60
+ end
61
+ value.present? ? augmented_klass.new(value) : nil
62
+ end
63
+
64
+ define_singleton_method :dump do |object|
65
+ if hash = Submodel.values(object).presence
66
+ case column.type
67
+ when :hstore, :json then hash
68
+ else YAML.dump(hash)
69
+ end
70
+ else
71
+ nil
72
+ end
73
+ end
74
+ }
75
+
76
+ # Include as module so we can override accessors and use `super`
77
+ include Module.new {
78
+ define_method attr do
79
+ self[attr] ||= augmented_klass.new
80
+ end
81
+
82
+ define_method :"#{attr}=" do |value|
83
+ if value.nil?
84
+ self[attr] = nil
85
+ elsif value.is_a? klass
86
+ self[attr] = value.dup
87
+ else
88
+ self[attr] = augmented_klass.new(value)
89
+ end
90
+ end
91
+ alias_method :"#{attr}_attributes=", :"#{attr}="
92
+ }
93
+
94
+ validates_each(attr, validation_options) do |record, attribute, object|
95
+ if object.try(:invalid?)
96
+ record.errors.add(attribute, object.errors.full_messages.to_sentence.downcase)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,3 @@
1
+ module Submodel
2
+ VERSION = '0.1.0'
3
+ end
data/lib/submodel.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'submodel/version'
2
+
3
+ require 'active_support/all'
4
+
5
+ module Submodel
6
+ end
7
+
8
+ ActiveSupport.on_load(:active_record) do
9
+ require 'submodel/active_record'
10
+ ActiveRecord::Base.send(:include, Submodel::ActiveRecord)
11
+ end
@@ -0,0 +1,48 @@
1
+ require 'active_record'
2
+ require 'carmen'
3
+
4
+ Dir[File.expand_path('../../spec/support/**/*.rb', __FILE__)].map(&method(:require))
5
+
6
+ RSpec.configure do |config|
7
+ config.include DatabaseMacros
8
+
9
+ config.before :suite do
10
+ DatabaseMacros.create_databases
11
+ end
12
+
13
+ config.filter_run :focus
14
+ config.run_all_when_everything_filtered = true
15
+
16
+ if config.files_to_run.one?
17
+ # RSpec filters the backtrace by default so as not to be so noisy.
18
+ # This causes the full backtrace to be printed when running a single
19
+ # spec file (e.g. to troubleshoot a particular spec failure).
20
+ config.full_backtrace = false
21
+
22
+ # Use the documentation formatter for detailed output,
23
+ # unless a formatter has already been configured
24
+ # (e.g. via a command-line flag).
25
+ config.formatter = 'doc' if config.formatters.none?
26
+ end
27
+
28
+ # Run specs in random order to surface order dependencies. If you find an
29
+ # order dependency and want to debug it, you can fix the order by providing
30
+ # the seed, which is printed after each run.
31
+ # --seed 1234
32
+ config.order = :random
33
+
34
+ # Seed global randomization in this process using the `--seed` CLI option.
35
+ # Setting this allows you to use `--seed` to deterministically reproduce
36
+ # test failures related to randomization by passing the same `--seed` value
37
+ # as the one that triggered the failure.
38
+ Kernel.srand config.seed
39
+
40
+ config.expect_with :rspec do |expectations|
41
+ expectations.syntax = :expect
42
+ end
43
+
44
+ config.mock_with :rspec do |mocks|
45
+ mocks.syntax = :expect
46
+ mocks.verify_partial_doubles = true
47
+ end
48
+ end
@@ -0,0 +1,354 @@
1
+ require 'submodel'
2
+
3
+ # Suppress annoying deprecation notice
4
+ I18n.enforce_available_locales = false
5
+
6
+ class Address
7
+ include ActiveModel::Model
8
+
9
+ COUNTRY_CODES = Carmen::Country.all.map(&:code)
10
+ US_STATE_CODES = Carmen::Country.coded('US').subregions.map(&:code)
11
+ CA_STATE_CODES = Carmen::Country.coded('CA').subregions.map(&:code)
12
+
13
+ attr_accessor :street_1, :street_2, :city, :state, :country, :postal_code
14
+
15
+ validates :country, inclusion: { in: COUNTRY_CODES }
16
+
17
+ with_options if: -> { country == 'US' } do |o|
18
+ o.validates :state, inclusion: { in: US_STATE_CODES }
19
+ o.validates :postal_code, format: /\d{5}/
20
+ end
21
+
22
+ with_options if: -> { country == 'CA' } do |o|
23
+ o.validates :state, inclusion: { in: CA_STATE_CODES }
24
+ o.validates :postal_code, format: /[a-z]\d[a-z]\W*\d[a-z]\d/i
25
+ end
26
+ end
27
+
28
+ shared_examples Submodel do
29
+ before do
30
+ Object.send(:remove_const, 'Order') if Object.const_defined? 'Order'
31
+
32
+ class Order < ActiveRecord::Base
33
+ submodel :billing_address, Address
34
+ submodel :shipping_address, Address, allow_blank: true do
35
+ validates :country, inclusion: { in: %w[US CA ME] }, allow_blank: true
36
+ end
37
+
38
+ # This tests that accessors are included as a module
39
+ def billing_address=(value)
40
+ super
41
+ end
42
+ def shipping_address
43
+ super
44
+ end
45
+ end
46
+ end
47
+
48
+ let(:valid_address_hash) { { 'country' => 'CA', :state => 'QC', 'postal_code' => 'G1K 3J3' } }
49
+ let(:valid_address) { Address.new(valid_address_hash) }
50
+ let(:order) { Order.new }
51
+
52
+ it 'class name is the same' do
53
+ expect(order.billing_address.class.name).to eq 'Address'
54
+ expect(order.shipping_address.class.name).to eq 'Address'
55
+ end
56
+
57
+ context 'when model attribute is nil' do
58
+ it 'leaves attribute as nil' do
59
+ expect(order.attributes).to eq({
60
+ 'id' => nil, 'billing_address' => nil, 'shipping_address' => nil
61
+ })
62
+ expect(order[:billing_address]).to eq nil
63
+ expect(order[:shipping_address]).to eq nil
64
+ end
65
+
66
+ describe 'getter' do
67
+ it 'sets attribute to blank submodel instance' do
68
+ expect(order.billing_address).to be_a Address
69
+ expect(order.billing_address.blank?).to eq true
70
+ end
71
+ end
72
+
73
+ describe 'setter' do
74
+ it 'sets attribute to object with passed value' do
75
+ order.billing_address = { street_1: '123 Fake Street' }
76
+ expect(order[:billing_address]).to be_a Address
77
+ expect(order[:billing_address].street_1).to eq '123 Fake Street'
78
+ end
79
+ end
80
+
81
+ describe '== comparison' do
82
+ it 'returns true when passed empty hash' do
83
+ expect(order.billing_address == {}).to eq true
84
+ end
85
+
86
+ it 'returns false when passed non-empty hash' do
87
+ expect(order.billing_address == { foo: 'bar' }).to eq false
88
+ end
89
+ end
90
+
91
+ [:inspect, :to_s].each do |meth|
92
+ describe "##{meth}" do
93
+ it 'outputs the right class name' do
94
+ expect(order.billing_address.public_send(meth)).to eq '#<Address>'
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ context 'when model attribute has value' do
101
+ let(:order) { Order.new(billing_address: { street_1: '123 Foo Street' }) }
102
+ let(:equivalent_address) { Address.new(street_1: '123 Foo Street', street_2: '') }
103
+ let(:different_address) { Address.new(street_1: '123 Bar Street') }
104
+
105
+ describe '== comparison' do
106
+ it 'returns true when passed equivalent object' do
107
+ expect(order.billing_address == equivalent_address).to eq true
108
+ end
109
+
110
+ it 'returns false when passed different object' do
111
+ expect(order.billing_address == different_address).to eq false
112
+ end
113
+
114
+ it 'returns false when passed different hash' do
115
+ expect(order.billing_address == { 'street_1' => 'blah blah blah' }).to eq false
116
+ end
117
+
118
+ it 'returns true when passed equivalent hash with string keys' do
119
+ expect(order.billing_address == { 'street_1' => '123 Foo Street' }).to eq true
120
+ end
121
+
122
+ it 'returns true when passed equivalent hash with symbol keys' do
123
+ expect(order.billing_address == { street_1: '123 Foo Street' }).to eq true
124
+ end
125
+
126
+ it 'returns false when passed irrelevant object' do
127
+ expect(order.billing_address == 1).to eq false
128
+ end
129
+ end
130
+
131
+ describe 'setter' do
132
+ it 'dups object when passed other object' do
133
+ order.billing_address = different_address
134
+ expect(order.billing_address.street_1).to eq '123 Bar Street'
135
+
136
+ order.billing_address.street_1 = 'blah blah blah'
137
+ expect(order.billing_address.street_1).to eq 'blah blah blah'
138
+ expect(different_address.street_1).to eq '123 Bar Street'
139
+ end
140
+
141
+ it 'sets attribute to nil when passed nil' do
142
+ order.billing_address = nil
143
+ expect(order[:billing_address]).to eq nil
144
+ end
145
+ end
146
+
147
+ [:inspect, :to_s].each do |meth|
148
+ describe "##{meth}" do
149
+ it 'outputs the right class name and variables' do
150
+ order.billing_address.street_2 = 'apt. 2'
151
+ expect(order.billing_address.public_send(meth)).to eq(
152
+ '#<Address street_1="123 Foo Street" street_2="apt. 2">'
153
+ )
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ describe 'persistence' do
160
+ context 'passing empty hash to setter' do
161
+ it 'persists NULL' do
162
+ order = Order.create!(
163
+ billing_address: valid_address,
164
+ shipping_address: {},
165
+ )
166
+ expect(raw_select(:orders, :shipping_address, id: order.id)).to eq nil
167
+
168
+ order = Order.first
169
+ expect(order.billing_address).to eq valid_address
170
+ expect(order.shipping_address).to eq({})
171
+ end
172
+ end
173
+
174
+ context 'passing hash with blank values to setter' do
175
+ it 'persists NULL' do
176
+ order = Order.create!(
177
+ billing_address: valid_address,
178
+ shipping_address: { street_1: '', city: ' ' },
179
+ )
180
+ expect(raw_select(:orders, :shipping_address, id: order.id)).to eq nil
181
+
182
+ order = Order.first
183
+ expect(order.billing_address).to eq valid_address
184
+ expect(order.shipping_address).to eq({})
185
+ end
186
+ end
187
+
188
+ context 'passing hash with non-blank values to setter' do |example|
189
+ it 'persists hash' do
190
+ order = Order.create!(
191
+ billing_address: valid_address,
192
+ shipping_address: { street_1: '123 Fake Street', city: 'Springfield', country: 'ME' },
193
+ )
194
+
195
+ case example.metadata[:billing_address_type]
196
+ when :hstore
197
+ billing_string = '"state"=>"QC", "country"=>"CA", "postal_code"=>"G1K 3J3"'
198
+ when :json
199
+ billing_string = '{"country":"CA","state":"QC","postal_code":"G1K 3J3"}'
200
+ else
201
+ billing_string = "---\ncountry: CA\nstate: QC\npostal_code: G1K 3J3\n"
202
+ end
203
+ expect(raw_select(:orders, :billing_address, id: order.id)).to eq billing_string
204
+
205
+ case example.metadata[:shipping_address_type]
206
+ when :hstore
207
+ shipping_string = '"city"=>"Springfield", "country"=>"ME", "street_1"=>"123 Fake Street"'
208
+ when :json
209
+ shipping_string = '{"street_1":"123 Fake Street","city":"Springfield","country":"ME"}'
210
+ else
211
+ shipping_string = "---\nstreet_1: 123 Fake Street\ncity: Springfield\ncountry: ME\n"
212
+ end
213
+ expect(raw_select(:orders, :shipping_address, id: order.id)).to eq shipping_string
214
+
215
+ order = Order.first
216
+ expect(order.billing_address).to eq valid_address
217
+ expect(order.shipping_address).to eq({
218
+ street_1: '123 Fake Street', city: 'Springfield', country: 'ME'
219
+ })
220
+ end
221
+ end
222
+
223
+ it 'doesn’t persist ActiveModel::Validation’s @error variable' do |example|
224
+ order = Order.new(billing_address: { state: 'QC', country: 'US' })
225
+ order.valid?
226
+ order.save! validate: false
227
+
228
+ case example.metadata[:billing_address_type]
229
+ when :hstore
230
+ billing_string = '"state"=>"QC", "country"=>"US"'
231
+ when :json
232
+ billing_string = '{"state":"QC","country":"US"}'
233
+ else
234
+ billing_string = "---\nstate: QC\ncountry: US\n"
235
+ end
236
+ expect(raw_select(:orders, :billing_address, id: order.id)).to eq billing_string
237
+ end
238
+ end
239
+
240
+ context 'when validating presence of submodel' do
241
+ let(:order) { Order.new(billing_address: { state: ' ', postal_code: '' }) }
242
+
243
+ it 'doesn’t accept blank object' do
244
+ expect(order.valid?).to eq false
245
+ expect(order.errors.keys).to include :billing_address
246
+ end
247
+ end
248
+
249
+ context 'when not validating presence of submodel' do
250
+ let(:order) {
251
+ Order.new(billing_address: valid_address_hash, shipping_address: { state: ' ', postal_code: '' })
252
+ }
253
+
254
+ it 'accepts blank object' do
255
+ expect(order.valid?).to eq true
256
+ end
257
+
258
+ it 'doesn’t accept invalid object' do
259
+ order.shipping_address = Address.new(country: 'CA', state: 'FOO')
260
+ expect(order.valid?).to eq false
261
+ expect(order.errors.keys).to include :shipping_address
262
+ end
263
+ end
264
+
265
+ context 'when extending one submodel column' do
266
+ let(:order) {
267
+ Order.new(
268
+ billing_address: { country: 'NL' },
269
+ shipping_address: { country: 'NL' },
270
+ )
271
+ }
272
+
273
+ it 'affects only the said column' do
274
+ expect(order.invalid?).to eq true
275
+ expect(order.errors.keys).to eq [:shipping_address]
276
+ end
277
+ end
278
+
279
+ describe 'submodel validation error message' do
280
+ it 'makes a sentence with all submodel errors' do
281
+ order = Order.new(
282
+ billing_address: { country: 'US', state: 'QC', postal_code: 'H0H 0H0' },
283
+ )
284
+ expect(order.valid?).to eq false
285
+ expect(order.errors.full_messages).to eq [
286
+ 'Billing address state is not included in the list and postal code is invalid'
287
+ ]
288
+ end
289
+ end
290
+
291
+ describe 'XXX_attributes= methods' do
292
+ it 'works' do
293
+ order = Order.new(billing_address_attributes: valid_address_hash)
294
+ expect(order.billing_address).to eq valid_address
295
+ end
296
+ end
297
+
298
+ describe 'attributes_before_type_cast' do
299
+ it 'does not break' do
300
+ order = Order.create!(billing_address: valid_address_hash, shipping_address: valid_address_hash)
301
+ expect(Order.last.attributes_before_type_cast.keys).to eq ['id', 'billing_address', 'shipping_address']
302
+ end
303
+ end
304
+ end
305
+
306
+ describe 'PostgreSQL', billing_address_type: :hstore, shipping_address_type: :json do
307
+ before do
308
+ ActiveRecord::Base.establish_connection(
309
+ adapter: 'postgresql', username: `whoami`.chomp, database: 'submodel_spec')
310
+
311
+ run_migration do
312
+ enable_extension :hstore
313
+
314
+ create_table :orders, force: true do |t|
315
+ t.hstore :billing_address
316
+ t.json :shipping_address
317
+ end
318
+ end
319
+ end
320
+
321
+ it_behaves_like Submodel
322
+ end
323
+
324
+ describe 'SQLite', billing_address_type: :text, shipping_address_type: :text do
325
+ before do
326
+ ActiveRecord::Base.establish_connection(
327
+ adapter: 'sqlite3', database: 'tmp/submodel.sqlite3')
328
+
329
+ run_migration do
330
+ create_table :orders, force: true do |t|
331
+ t.text :billing_address
332
+ t.text :shipping_address
333
+ end
334
+ end
335
+ end
336
+
337
+ it_behaves_like Submodel
338
+ end
339
+
340
+ describe 'MySQL', billing_address_type: :text, shipping_address_type: :text do
341
+ before do
342
+ ActiveRecord::Base.establish_connection(
343
+ adapter: 'mysql2', username: 'root', database: 'submodel_spec')
344
+
345
+ run_migration do
346
+ create_table :orders, force: true do |t|
347
+ t.text :billing_address
348
+ t.text :shipping_address
349
+ end
350
+ end
351
+ end
352
+
353
+ it_behaves_like Submodel
354
+ end
@@ -0,0 +1,25 @@
1
+ module DatabaseMacros
2
+ def self.create_databases
3
+ [
4
+ { adapter: 'postgresql', username: `whoami`.chomp, database: 'postgres' },
5
+ { adapter: 'mysql2', username: 'root', database: 'mysql' },
6
+ ].each do |config|
7
+ ActiveRecord::Base.establish_connection(config)
8
+ ActiveRecord::Base.connection.drop_database 'submodel_spec' rescue nil
9
+ ActiveRecord::Base.connection.create_database 'submodel_spec'
10
+ end
11
+
12
+ ActiveRecord::Base.logger = ActiveRecord::Migration.verbose = false
13
+ end
14
+
15
+ # Taken from https://github.com/mirego/partisan/blob/master/spec/support/macros/database_macros.rb
16
+ def run_migration(&block)
17
+ klass = Class.new(ActiveRecord::Migration)
18
+ klass.send(:define_method, :up) { instance_exec &block }
19
+ klass.new.up
20
+ end
21
+
22
+ def raw_select(table, column, id: nil)
23
+ ActiveRecord::Base.connection.select_one("SELECT #{column} FROM #{table} WHERE id = #{id}")[column.to_s]
24
+ end
25
+ end
data/submodel.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'submodel/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'submodel'
8
+ spec.version = Submodel::VERSION
9
+ spec.authors = ['Rafaël Blais Masson']
10
+ spec.email = ['rafbmasson@gmail.com']
11
+ spec.summary = 'Submodel maps ActiveRecord columns to ActiveModel models.'
12
+ spec.homepage = 'http://github.com/rafBM/submodel'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'activerecord', '>= 3.0'
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.6'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec', '>= 3.0.0.beta2'
25
+ spec.add_development_dependency 'pg'
26
+ spec.add_development_dependency 'sqlite3'
27
+ spec.add_development_dependency 'mysql2'
28
+ spec.add_development_dependency 'carmen'
29
+ end
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: submodel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rafaël Blais Masson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0.beta2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0.beta2
69
+ - !ruby/object:Gem::Dependency
70
+ name: pg
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: mysql2
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: carmen
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description:
126
+ email:
127
+ - rafbmasson@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rspec"
134
+ - Gemfile
135
+ - LICENSE.txt
136
+ - README.md
137
+ - Rakefile
138
+ - lib/submodel.rb
139
+ - lib/submodel/active_record.rb
140
+ - lib/submodel/version.rb
141
+ - spec/spec_helper.rb
142
+ - spec/submodel_spec.rb
143
+ - spec/support/macros/database_macros.rb
144
+ - submodel.gemspec
145
+ homepage: http://github.com/rafBM/submodel
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 2.2.2
166
+ signing_key:
167
+ specification_version: 4
168
+ summary: Submodel maps ActiveRecord columns to ActiveModel models.
169
+ test_files:
170
+ - spec/spec_helper.rb
171
+ - spec/submodel_spec.rb
172
+ - spec/support/macros/database_macros.rb