virtus_model 0.2.5

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: f21f80530d6cc87de3ae040817f78c8c83b4567f
4
+ data.tar.gz: 572466a814d7c485b6bd89aaed1614e29b837b4a
5
+ SHA512:
6
+ metadata.gz: 75489e6017e171d19a196a82f228713682728331c2a58ca0e6d1da4a33cebeb590128053b90a21afe4482b7be6b256f05b5782667f1dfb22335eae7aff39618d
7
+ data.tar.gz: c39b394204e97a0f26b49693cea7addd5e34fedb7366a5626e9a25567a46333441e13d677d02018c235ddda0ef011914d619a193b7f50cb754ab131136d19f6c
@@ -0,0 +1,165 @@
1
+ require 'virtus'
2
+ require 'active_model'
3
+
4
+ module VirtusModel
5
+ class Base
6
+ include ActiveModel::Conversion
7
+ include ActiveModel::Validations
8
+ include Virtus.model
9
+
10
+ set_callback :validate, :validate_associations
11
+
12
+ # Get an array of attribute names.
13
+ def self.attributes
14
+ attribute_set.map(&:name)
15
+ end
16
+
17
+ # Is there an attribute with the provided name?
18
+ def self.attribute?(name)
19
+ attributes.include?(name)
20
+ end
21
+
22
+ # Is there an association with the provided name and type (optional)?
23
+ def self.association?(name, *types)
24
+ associations(*types).include?(name)
25
+ end
26
+
27
+ # Get an array of association names by type (optional).
28
+ def self.associations(*types)
29
+ classes = {
30
+ one: Virtus::Attribute::EmbeddedValue,
31
+ many: Virtus::Attribute::Collection
32
+ }.select do |type, _|
33
+ types.empty? || types.include?(type)
34
+ end
35
+
36
+ attribute_set.select do |field|
37
+ classes.any? { |_, cls| field.class <= cls }
38
+ end.map(&:name)
39
+ end
40
+
41
+ # Initialize attributes using the provided hash or object.
42
+ def initialize(model = nil)
43
+ super(compact_hash(extract_attributes(model)))
44
+ end
45
+
46
+ # Recursively update attributes and return a self-reference.
47
+ def assign_attributes(model)
48
+ self.attributes = extract_attributes(model)
49
+ self
50
+ end
51
+
52
+ # Update attributes and validate.
53
+ def update(model = nil, options = {})
54
+ assign_attributes(model)
55
+ validate(options)
56
+ end
57
+
58
+ # Two models are equal if their attributes are equal.
59
+ def ==(other)
60
+ if other.is_a?(VirtusModel::Base)
61
+ self.attributes == other.attributes
62
+ else
63
+ self.attributes == other
64
+ end
65
+ end
66
+
67
+ # Recursively convert all attributes to hash pairs.
68
+ def export(options = nil)
69
+ self.class.attributes.reduce({}) do |result, name|
70
+ value = attributes[name]
71
+ if self.class.association?(name, :many)
72
+ result[name] = export_values(value, options)
73
+ elsif self.class.association?(name, :one)
74
+ result[name] = export_value(value, options)
75
+ else
76
+ result[name] = value
77
+ end
78
+ result
79
+ end
80
+ end
81
+
82
+ # Alias of #export.
83
+ def to_hash(options = nil)
84
+ export(options)
85
+ end
86
+
87
+ # Alias of #to_hash.
88
+ def to_h(options = nil)
89
+ to_hash(options)
90
+ end
91
+
92
+ # Alias of #export.
93
+ def as_json(options = nil)
94
+ export(options)
95
+ end
96
+
97
+ # Convert the #as_json result to JSON.
98
+ def to_json(options = nil)
99
+ as_json(options).to_json
100
+ end
101
+
102
+ protected
103
+
104
+ # Extract model attributes into a hash.
105
+ def extract_attributes(model)
106
+ self.class.attributes.reduce({}) do |result, name|
107
+ if model.respond_to?(name)
108
+ result[name] = model.public_send(name)
109
+ elsif model.respond_to?(:[])
110
+ result[name] = model[name]
111
+ end
112
+ result
113
+ end
114
+ end
115
+
116
+ # Validate all associations by type and import resulting errors.
117
+ def validate_associations
118
+ validate_associations_one
119
+ validate_associations_many
120
+ end
121
+
122
+ # Validate "one" associations and import errors.
123
+ def validate_associations_one
124
+ self.class.associations(:one).each do |name|
125
+ import_errors(name, attributes[name])
126
+ end
127
+ end
128
+
129
+ # Validate "many" associations and import errors.
130
+ def validate_associations_many
131
+ self.class.associations(:many).each do |name|
132
+ values = attributes[name] || []
133
+ values.each.with_index do |value, index|
134
+ import_errors("#{name}[#{index}]", value)
135
+ end
136
+ end
137
+ end
138
+
139
+ # Merge associated errors using the current validation context.
140
+ def import_errors(name, model)
141
+ return unless model.respond_to?(:validate)
142
+ return if model.validate(validation_context)
143
+ model.errors.each do |field, error|
144
+ errors.add("#{name}[#{field}]", error)
145
+ end
146
+ end
147
+
148
+ # Export each value with the provided options.
149
+ def export_values(values, options = nil)
150
+ return if values.nil?
151
+ values.map { |v| export_value(v, options) }
152
+ end
153
+
154
+ # Export the value with the provided options.
155
+ def export_value(value, options = nil)
156
+ return if value.nil?
157
+ value.respond_to?(:export) ? value.export(options) : value
158
+ end
159
+
160
+ # Omit keys with nil values.
161
+ def compact_hash(hash)
162
+ hash.select { |_, value| !value.nil? }
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,3 @@
1
+ module VirtusModel
2
+ VERSION = '0.2.5'.freeze
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'virtus_model/base'
2
+ require 'virtus_model/version'
@@ -0,0 +1,16 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'bundler/setup'
5
+ Bundler.setup
6
+
7
+ require 'virtus_model'
8
+ require 'shoulda/matchers'
9
+ require 'active_support/core_ext/object/try'
10
+
11
+ Shoulda::Matchers.configure do |config|
12
+ config.integrate do |with|
13
+ with.test_framework :rspec
14
+ with.library :active_model
15
+ end
16
+ end
@@ -0,0 +1,355 @@
1
+ describe VirtusModel::Base do
2
+ class SimpleModel < VirtusModel::Base
3
+ attribute :name, String
4
+ validates :name, presence: true
5
+ end
6
+
7
+ let(:simple_model) { SimpleModel.new(simple_model_attributes) }
8
+ let(:simple_model_attributes) { { name: 'test' } }
9
+
10
+ describe SimpleModel, type: :model do
11
+ it { is_expected.to validate_presence_of(:name) }
12
+ end
13
+
14
+ class ComplexModel < VirtusModel::Base
15
+ attribute :model, SimpleModel
16
+ attribute :models, Array[SimpleModel]
17
+ validates :model, presence: true
18
+ validates :models, presence: true
19
+ end
20
+
21
+ let(:complex_model) { ComplexModel.new(complex_model_attributes) }
22
+ let(:complex_model_attributes) do
23
+ {
24
+ model: simple_model_attributes,
25
+ models: [simple_model_attributes]
26
+ }
27
+ end
28
+
29
+ describe ComplexModel, type: :model do
30
+ it { is_expected.to validate_presence_of(:model) }
31
+ it { is_expected.to validate_presence_of(:models) }
32
+ end
33
+
34
+ class InheritedModel < ComplexModel
35
+ attribute :name, String
36
+ end
37
+
38
+ describe InheritedModel, type: :model do
39
+ it { is_expected.to validate_presence_of(:model) }
40
+ it { is_expected.to validate_presence_of(:models) }
41
+ end
42
+
43
+ describe '.attribute?' do
44
+ context SimpleModel do
45
+ subject { SimpleModel }
46
+
47
+ it { expect(subject.attribute?(:name)).to be(true) }
48
+ it { expect(subject.attribute?(:other)).to be(false) }
49
+ end
50
+
51
+ context ComplexModel do
52
+ subject { ComplexModel }
53
+
54
+ it { expect(subject.attribute?(:model)).to be(true) }
55
+ it { expect(subject.attribute?(:models)).to be(true) }
56
+ it { expect(subject.attribute?(:other)).to be(false) }
57
+ end
58
+ end
59
+
60
+ describe '.association?' do
61
+ context SimpleModel do
62
+ subject { SimpleModel }
63
+
64
+ it { expect(subject.association?(:name)).to be(false) }
65
+ it { expect(subject.association?(:other)).to be(false) }
66
+ end
67
+
68
+ context ComplexModel do
69
+ subject { ComplexModel }
70
+
71
+ it { expect(subject.association?(:model)).to be(true) }
72
+ it { expect(subject.association?(:models)).to be(true) }
73
+ it { expect(subject.association?(:other)).to be(false) }
74
+ end
75
+ end
76
+
77
+ describe '.associations' do
78
+ context SimpleModel do
79
+ subject { SimpleModel }
80
+
81
+ it { expect(subject.associations).to eq([]) }
82
+ end
83
+
84
+ context ComplexModel do
85
+ subject { ComplexModel }
86
+
87
+ it { expect(subject.associations).to eq([:model, :models]) }
88
+ it { expect(subject.associations(:one)).to eq([:model]) }
89
+ it { expect(subject.associations(:many)).to eq([:models]) }
90
+ end
91
+ end
92
+
93
+ describe '#initialize' do
94
+ context SimpleModel do
95
+ subject { SimpleModel.new(attributes) }
96
+
97
+ context 'attributes are blank' do
98
+ let(:attributes) { nil }
99
+ it { expect(subject.attributes).to eq(name: nil) }
100
+ end
101
+
102
+ context 'attributes are present' do
103
+ let(:attributes) { { name: 'test', other: 'test' } }
104
+ it { expect(subject.attributes).to eq(name: 'test') }
105
+ it { expect(subject.attributes).not_to eq(other: 'test') }
106
+ end
107
+ end
108
+
109
+ context ComplexModel do
110
+ subject { ComplexModel.new(attributes) }
111
+
112
+ context 'attributes are blank' do
113
+ let(:attributes) { nil }
114
+ it { expect(subject.attributes).to eq(model: nil, models: []) }
115
+ end
116
+
117
+ context 'attributes are present' do
118
+ let(:attributes) { { model: model, models: [model], other: 'test' } }
119
+ let(:model) { { name: 'test' } }
120
+ it { expect(subject.export[:model]).to eq(model) }
121
+ it { expect(subject.export[:models]).to eq([model]) }
122
+ it { expect(subject.attributes).not_to eq(other: 'test') }
123
+ end
124
+ end
125
+ end
126
+
127
+ describe '#assign_attributes' do
128
+ context SimpleModel do
129
+ subject { SimpleModel.new.assign_attributes(attributes) }
130
+
131
+ context 'hash' do
132
+ let(:attributes) { { name: 'test', other: 'test' } }
133
+ it { expect(subject.attributes).to include(name: 'test') }
134
+ it { expect(subject.attributes).not_to include(other: 'test') }
135
+ end
136
+
137
+ context 'object' do
138
+ let(:attributes) { simple_model }
139
+ it { expect(subject == simple_model).to be(true) }
140
+ end
141
+ end
142
+
143
+ context ComplexModel do
144
+ subject { ComplexModel.new.assign_attributes(attributes) }
145
+
146
+ context 'hash' do
147
+ let(:attributes) { { model: model, models: [model], other: 'test' } }
148
+ let(:model) { { name: 'test', other: 'test' } }
149
+ it { expect(subject.attributes[:model]).to eq(SimpleModel.new(model)) }
150
+ it { expect(subject.attributes[:models]).to eq([SimpleModel.new(model)]) }
151
+ it { expect(subject.attributes).not_to include(other: 'test') }
152
+ end
153
+
154
+ context 'object' do
155
+ let(:attributes) { complex_model }
156
+ it { expect(subject == complex_model).to be(true) }
157
+ end
158
+ end
159
+ end
160
+
161
+ describe '#update' do
162
+ let!(:valid) { subject.update(attributes) }
163
+
164
+ context SimpleModel do
165
+ subject { SimpleModel.new }
166
+
167
+ context 'attributes are blank' do
168
+ let(:attributes) { nil }
169
+ it { expect(subject.export).to eq(name: nil) }
170
+ it { expect(valid).to be(false) }
171
+ end
172
+
173
+ context 'attributes are invalid' do
174
+ let(:attributes) { { name: '' } }
175
+ it { expect(subject.export).to eq(name: '') }
176
+ it { expect(valid).to be(false) }
177
+ end
178
+
179
+ context 'attributes are valid' do
180
+ let(:attributes) { simple_model_attributes }
181
+ it { expect(subject.export).to eq(name: 'test') }
182
+ it { expect(valid).to be(true) }
183
+ end
184
+ end
185
+
186
+ context ComplexModel do
187
+ subject { ComplexModel.new }
188
+
189
+ context 'attributes are blank' do
190
+ let(:attributes) { nil }
191
+ it { expect(subject.export).to eq(model: nil, models: []) }
192
+ it { expect(valid).to be(false) }
193
+ end
194
+
195
+ context 'attributes are invalid' do
196
+ let(:attributes) { { model: {} } }
197
+ it { expect(subject.export[:model]).to include(name: nil) }
198
+ it { expect(valid).to be(false) }
199
+ end
200
+
201
+ context 'attributes are valid' do
202
+ let(:attributes) { complex_model_attributes }
203
+ it { expect(subject.export[:model]).to include(name: 'test') }
204
+ it { expect(valid).to be(true) }
205
+ end
206
+ end
207
+ end
208
+
209
+ describe '#==' do
210
+ context SimpleModel do
211
+ subject { simple_model }
212
+
213
+ context 'equal hash' do
214
+ let(:other) { subject.dup.export }
215
+ it { expect(subject == other).to be(true) }
216
+ end
217
+
218
+ context 'equal object' do
219
+ let(:other) { subject.dup }
220
+ it { expect(subject == other).to be(true) }
221
+ end
222
+
223
+ context 'unequal hash' do
224
+ let(:other) { subject.dup.export.merge(name: 'test2') }
225
+ it { expect(subject == other).to be(false) }
226
+ end
227
+
228
+ context 'unequal object' do
229
+ let(:other) { subject.dup.assign_attributes(name: 'test2') }
230
+ it { expect(subject == other).to be(false) }
231
+ end
232
+ end
233
+
234
+ context ComplexModel do
235
+ subject { complex_model }
236
+
237
+ context 'equal hash' do
238
+ let(:other) { subject.dup.export }
239
+ it { expect(subject == other).to be(true) }
240
+ end
241
+
242
+ context 'equal object' do
243
+ let(:other) { subject.dup }
244
+ it { expect(subject == other).to be(true) }
245
+ end
246
+
247
+ context 'unequal hash' do
248
+ let(:other) { subject.dup.export.merge(models: { name: 'test2' }) }
249
+ it { expect(subject == other).to be(false) }
250
+ end
251
+
252
+ context 'unequal object' do
253
+ let(:other) { subject.dup.assign_attributes(models: [{ name: 'test2' }]) }
254
+ it { expect(subject == other).to be(false) }
255
+ end
256
+ end
257
+ end
258
+
259
+ describe '#export' do
260
+ context SimpleModel do
261
+ subject { simple_model }
262
+
263
+ it { expect(subject.export).to eq(simple_model_attributes) }
264
+ end
265
+
266
+ context ComplexModel do
267
+ subject { complex_model }
268
+
269
+ it { expect(subject.export).to eq(complex_model_attributes) }
270
+ end
271
+ end
272
+
273
+ describe '#to_hash' do
274
+ context SimpleModel do
275
+ subject { simple_model }
276
+
277
+ it { expect(subject.to_hash).to eq(subject.export) }
278
+ it { expect(subject.to_h).to eq(subject.to_hash) }
279
+ it { expect(subject.as_json).to eq(subject.to_hash) }
280
+ end
281
+
282
+ context ComplexModel do
283
+ subject { complex_model }
284
+
285
+ it { expect(subject.to_hash).to eq(subject.export) }
286
+ it { expect(subject.to_h).to eq(subject.to_hash) }
287
+ it { expect(subject.as_json).to eq(subject.to_hash) }
288
+ end
289
+ end
290
+
291
+ describe '#to_json' do
292
+ context SimpleModel do
293
+ subject { simple_model }
294
+
295
+ it { expect(subject.to_json).to eq(subject.export.to_json) }
296
+ end
297
+
298
+ context ComplexModel do
299
+ subject { complex_model }
300
+
301
+ it { expect(subject.to_json).to eq(subject.export.to_json) }
302
+ end
303
+ end
304
+
305
+ describe '#validate' do
306
+ before { subject.validate }
307
+
308
+ context SimpleModel do
309
+ context 'valid' do
310
+ subject { simple_model }
311
+
312
+ it { expect(subject.valid?).to be(true) }
313
+ it { expect(subject.errors).to be_blank }
314
+ end
315
+
316
+ context 'invalid' do
317
+ subject { simple_model.assign_attributes(name: '') }
318
+
319
+ it { expect(subject.valid?).to be(false) }
320
+ it { expect(subject.errors.messages).to include(name: ["can't be blank"]) }
321
+ end
322
+ end
323
+
324
+ context ComplexModel do
325
+ context 'valid' do
326
+ subject { complex_model }
327
+
328
+ it { expect(subject.valid?).to be(true) }
329
+ it { expect(subject.errors).to be_blank }
330
+ end
331
+
332
+ context 'invalid' do
333
+ subject { complex_model.assign_attributes(model: {}, models: [{}]) }
334
+
335
+ it { expect(subject.valid?).to be(false) }
336
+ it { expect(subject.errors.messages).to include(:"model[name]" => ["can't be blank"]) }
337
+ it { expect(subject.errors.messages).to include(:"models[0][name]" => ["can't be blank"]) }
338
+ end
339
+ end
340
+ end
341
+
342
+ describe '#to_json' do
343
+ context SimpleModel do
344
+ subject { simple_model }
345
+
346
+ it { expect(subject.to_json).to eq(subject.export.to_json) }
347
+ end
348
+
349
+ context ComplexModel do
350
+ subject { complex_model }
351
+
352
+ it { expect(subject.to_json).to eq(subject.export.to_json) }
353
+ end
354
+ end
355
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: virtus_model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.5
5
+ platform: ruby
6
+ authors:
7
+ - Derek Schaefer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: virtus
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activemodel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '11.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '11.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rdoc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: shoulda-matchers
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.11'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.11'
111
+ description: VirtusModel
112
+ email:
113
+ - derek.schaefer@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/virtus_model.rb
119
+ - lib/virtus_model/base.rb
120
+ - lib/virtus_model/version.rb
121
+ - spec/spec_helper.rb
122
+ - spec/virtus_model/base_spec.rb
123
+ homepage: https://github.com/derek-schaefer/virtus_model
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: 2.0.0
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.5.1
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: VirtusModel
147
+ test_files:
148
+ - spec/spec_helper.rb
149
+ - spec/virtus_model/base_spec.rb