submodel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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