virtus_model 0.2.5

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