voids 2.0.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 +7 -0
- data/.circleci/config.yml +11 -0
- data/.rspec +2 -0
- data/.rubocop.yml +356 -0
- data/.rubocop_todo.yml +126 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +427 -0
- data/Rakefile +8 -0
- data/examples/advanced_features_example.rb +81 -0
- data/examples/assign_from_model_example.rb +54 -0
- data/examples/model_name_example.rb +31 -0
- data/examples/normalization_example.rb +39 -0
- data/examples/post_form_example.rb +52 -0
- data/lib/voids/association_proxy.rb +91 -0
- data/lib/voids/associations.rb +59 -0
- data/lib/voids/base.rb +252 -0
- data/lib/voids/model_naming.rb +25 -0
- data/lib/voids/nested_attributes.rb +128 -0
- data/lib/voids/normalization.rb +39 -0
- data/lib/voids/version.rb +5 -0
- data/lib/voids.rb +17 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/voids/association_proxy_spec.rb +213 -0
- data/spec/voids/associations_spec.rb +185 -0
- data/spec/voids/attributes_extraction_spec.rb +138 -0
- data/spec/voids/base_spec.rb +361 -0
- data/spec/voids/callbacks_spec.rb +60 -0
- data/spec/voids/custom_primary_key_spec.rb +168 -0
- data/spec/voids/dirty_tracking_spec.rb +61 -0
- data/spec/voids/i18n_spec.rb +33 -0
- data/spec/voids/id_tracking_spec.rb +168 -0
- data/spec/voids/inherit_attributes_from_spec.rb +149 -0
- data/spec/voids/inherit_validations_from_spec.rb +260 -0
- data/spec/voids/model_naming_spec.rb +82 -0
- data/spec/voids/nested_attributes_spec.rb +377 -0
- data/spec/voids/normalization_spec.rb +122 -0
- metadata +125 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Voids::Base do
|
|
6
|
+
describe '.attribute' do
|
|
7
|
+
it 'defines an attribute with a type' do
|
|
8
|
+
form_class = Class.new(described_class) do
|
|
9
|
+
attribute :title, :string
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
form = form_class.new(title: 'hello')
|
|
13
|
+
|
|
14
|
+
expect(form.title).to eq('hello')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'supports default values' do
|
|
18
|
+
form_class = Class.new(described_class) do
|
|
19
|
+
attribute :status, :string, default: 'draft'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
form = form_class.new
|
|
23
|
+
|
|
24
|
+
expect(form.status).to eq('draft')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'supports default values from proc' do
|
|
28
|
+
form_class = Class.new(described_class) do
|
|
29
|
+
attribute :timestamp, :datetime, default: -> { Time.new(2025, 1, 1) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
form = form_class.new
|
|
33
|
+
|
|
34
|
+
expect(form.timestamp).to eq(Time.new(2025, 1, 1))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#initialize' do
|
|
39
|
+
it 'accepts a hash of attributes' do
|
|
40
|
+
form_class = Class.new(described_class) do
|
|
41
|
+
attribute :title, :string
|
|
42
|
+
attribute :count, :integer
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
form = form_class.new(title: 'test', count: 5)
|
|
46
|
+
|
|
47
|
+
expect(form.title).to eq('test')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'accepts an empty hash' do
|
|
51
|
+
form_class = Class.new(described_class)
|
|
52
|
+
|
|
53
|
+
form = form_class.new({})
|
|
54
|
+
|
|
55
|
+
expect(form).to be_a(described_class)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe '#assign_attributes' do
|
|
60
|
+
it 'assigns multiple attributes' do
|
|
61
|
+
form_class = Class.new(described_class) do
|
|
62
|
+
attribute :title, :string
|
|
63
|
+
attribute :content, :string
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
form = form_class.new
|
|
67
|
+
form.assign_attributes(title: 'new title', content: 'new content')
|
|
68
|
+
|
|
69
|
+
expect(form.title).to eq('new title')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'handles nested attributes with _attributes suffix' do
|
|
73
|
+
nested_class = Class.new(described_class) do
|
|
74
|
+
attribute :url, :string
|
|
75
|
+
end
|
|
76
|
+
stub_const('PhotoForm', nested_class)
|
|
77
|
+
|
|
78
|
+
form_class = Class.new(described_class) do
|
|
79
|
+
has_one :photo
|
|
80
|
+
attribute :title, :string
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
form = form_class.new
|
|
84
|
+
form.assign_attributes(photo_attributes: { url: 'https://example.com' })
|
|
85
|
+
|
|
86
|
+
expect(form.photo.url).to eq('https://example.com')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe '.from_model' do
|
|
91
|
+
it 'creates new instance from model' do
|
|
92
|
+
model = Struct.new(:title, :content).new('model title', 'model content')
|
|
93
|
+
|
|
94
|
+
form_class = Class.new(described_class) do
|
|
95
|
+
attribute :title, :string
|
|
96
|
+
attribute :content, :string
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
form = form_class.from_model(model)
|
|
100
|
+
|
|
101
|
+
expect(form.title).to eq('model title')
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe '#from_model' do
|
|
106
|
+
it 'copies matching attributes from model' do
|
|
107
|
+
model = Struct.new(:title, :content).new('model title', 'model content')
|
|
108
|
+
|
|
109
|
+
form_class = Class.new(described_class) do
|
|
110
|
+
attribute :title, :string
|
|
111
|
+
attribute :content, :string
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
form = form_class.new
|
|
115
|
+
form.from_model(model)
|
|
116
|
+
|
|
117
|
+
expect(form.title).to eq('model title')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'returns self for chaining' do
|
|
121
|
+
model = Struct.new(:title).new('title')
|
|
122
|
+
|
|
123
|
+
form_class = Class.new(described_class) do
|
|
124
|
+
attribute :title, :string
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
form = form_class.new
|
|
128
|
+
result = form.from_model(model)
|
|
129
|
+
|
|
130
|
+
expect(result).to eq(form)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'ignores attributes not defined on form' do
|
|
134
|
+
model = Struct.new(:title, :extra).new('title', 'extra')
|
|
135
|
+
|
|
136
|
+
form_class = Class.new(described_class) do
|
|
137
|
+
attribute :title, :string
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
form = form_class.new
|
|
141
|
+
form.from_model(model)
|
|
142
|
+
|
|
143
|
+
expect(form.title).to eq('title')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'handles nil model' do
|
|
147
|
+
form_class = Class.new(described_class) do
|
|
148
|
+
attribute :title, :string
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
form = form_class.new
|
|
152
|
+
form.from_model(nil)
|
|
153
|
+
|
|
154
|
+
expect(form.title).to be_nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'assigns has_one associations from model' do
|
|
158
|
+
model_photo = Struct.new(:url).new('https://example.com/photo.jpg')
|
|
159
|
+
model = Struct.new(:title, :photo).new('test', model_photo)
|
|
160
|
+
|
|
161
|
+
photo_form_class = Class.new(described_class) do
|
|
162
|
+
attribute :url, :string
|
|
163
|
+
end
|
|
164
|
+
stub_const('PhotoForm', photo_form_class)
|
|
165
|
+
|
|
166
|
+
form_class = Class.new(described_class) do
|
|
167
|
+
has_one :photo
|
|
168
|
+
attribute :title, :string
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
form = form_class.new
|
|
172
|
+
form.from_model(model)
|
|
173
|
+
|
|
174
|
+
expect(form.photo.url).to eq('https://example.com/photo.jpg')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'assigns has_many associations from model' do
|
|
178
|
+
model_image1 = Struct.new(:url).new('https://example.com/1.jpg')
|
|
179
|
+
model_image2 = Struct.new(:url).new('https://example.com/2.jpg')
|
|
180
|
+
model = Struct.new(:title, :images).new('test', [model_image1, model_image2])
|
|
181
|
+
|
|
182
|
+
image_form_class = Class.new(described_class) do
|
|
183
|
+
attribute :url, :string
|
|
184
|
+
end
|
|
185
|
+
stub_const('ImageForm', image_form_class)
|
|
186
|
+
|
|
187
|
+
form_class = Class.new(described_class) do
|
|
188
|
+
has_many :images
|
|
189
|
+
attribute :title, :string
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
form = form_class.new
|
|
193
|
+
form.from_model(model)
|
|
194
|
+
|
|
195
|
+
expect(form.images.count).to eq(2)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
describe '#persisted?' do
|
|
200
|
+
it 'returns false when id is not present' do
|
|
201
|
+
form_class = Class.new(described_class) do
|
|
202
|
+
attribute :id, :integer
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
form = form_class.new
|
|
206
|
+
|
|
207
|
+
expect(form.persisted?).to be(false)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'returns true when id is present' do
|
|
211
|
+
form_class = Class.new(described_class) do
|
|
212
|
+
attribute :id, :integer
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
form = form_class.new(id: 123)
|
|
216
|
+
|
|
217
|
+
expect(form.persisted?).to be(true)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'returns false when id attribute is not defined' do
|
|
221
|
+
form_class = Class.new(described_class)
|
|
222
|
+
|
|
223
|
+
form = form_class.new
|
|
224
|
+
|
|
225
|
+
expect(form.persisted?).to be(false)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
describe '#to_key' do
|
|
230
|
+
it 'returns nil when not persisted' do
|
|
231
|
+
form_class = Class.new(described_class) do
|
|
232
|
+
attribute :id, :integer
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
form = form_class.new
|
|
236
|
+
|
|
237
|
+
expect(form.to_key).to be_nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'returns array with id when persisted' do
|
|
241
|
+
form_class = Class.new(described_class) do
|
|
242
|
+
attribute :id, :integer
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
form = form_class.new(id: 456)
|
|
246
|
+
|
|
247
|
+
expect(form.to_key).to eq([456])
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
describe '#to_param' do
|
|
252
|
+
it 'returns nil when not persisted' do
|
|
253
|
+
form_class = Class.new(described_class) do
|
|
254
|
+
attribute :id, :integer
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
form = form_class.new
|
|
258
|
+
|
|
259
|
+
expect(form.to_param).to be_nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'returns id as string when persisted' do
|
|
263
|
+
form_class = Class.new(described_class) do
|
|
264
|
+
attribute :id, :integer
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
form = form_class.new(id: 789)
|
|
268
|
+
|
|
269
|
+
expect(form.to_param).to eq('789')
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
describe '#to_model' do
|
|
274
|
+
it 'returns self' do
|
|
275
|
+
form_class = Class.new(described_class)
|
|
276
|
+
|
|
277
|
+
form = form_class.new
|
|
278
|
+
|
|
279
|
+
expect(form.to_model).to eq(form)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
describe '#valid?' do
|
|
284
|
+
it 'returns true when all validations pass' do
|
|
285
|
+
form_class = Class.new(described_class) do
|
|
286
|
+
attribute :title, :string
|
|
287
|
+
validates :title, presence: true
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
form = form_class.new(title: 'present')
|
|
291
|
+
|
|
292
|
+
expect(form.valid?).to be(true)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it 'returns false when validations fail' do
|
|
296
|
+
form_class = Class.new(described_class) do
|
|
297
|
+
attribute :title, :string
|
|
298
|
+
validates :title, presence: true
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
form = form_class.new
|
|
302
|
+
|
|
303
|
+
expect(form.valid?).to be(false)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it 'validates nested has_one forms' do
|
|
307
|
+
nested_class = Class.new(described_class) do
|
|
308
|
+
attribute :url, :string
|
|
309
|
+
validates :url, presence: true
|
|
310
|
+
end
|
|
311
|
+
stub_const('PhotoForm', nested_class)
|
|
312
|
+
|
|
313
|
+
form_class = Class.new(described_class) do
|
|
314
|
+
has_one :photo
|
|
315
|
+
attribute :title, :string
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
form = form_class.new(title: 'test')
|
|
319
|
+
form.build_photo
|
|
320
|
+
|
|
321
|
+
expect(form.valid?).to be(false)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'validates nested has_many forms' do
|
|
325
|
+
nested_class = Class.new(described_class) do
|
|
326
|
+
attribute :url, :string
|
|
327
|
+
validates :url, presence: true
|
|
328
|
+
end
|
|
329
|
+
stub_const('ImageForm', nested_class)
|
|
330
|
+
|
|
331
|
+
form_class = Class.new(described_class) do
|
|
332
|
+
has_many :images
|
|
333
|
+
attribute :title, :string
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
form = form_class.new(title: 'test')
|
|
337
|
+
form.images.new
|
|
338
|
+
|
|
339
|
+
expect(form.valid?).to be(false)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it 'includes nested form errors in parent errors' do
|
|
343
|
+
nested_class = Class.new(described_class) do
|
|
344
|
+
attribute :url, :string
|
|
345
|
+
validates :url, presence: true
|
|
346
|
+
end
|
|
347
|
+
stub_const('PhotoForm', nested_class)
|
|
348
|
+
|
|
349
|
+
form_class = Class.new(described_class) do
|
|
350
|
+
has_one :photo
|
|
351
|
+
attribute :title, :string
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
form = form_class.new(title: 'test')
|
|
355
|
+
form.build_photo
|
|
356
|
+
form.valid?
|
|
357
|
+
|
|
358
|
+
expect(form.errors.messages.keys).to include(:'photo.url')
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Callbacks' do
|
|
6
|
+
it 'runs before_validation callback' do
|
|
7
|
+
form_class = Class.new(Voids::Base) do
|
|
8
|
+
attribute :title, :string
|
|
9
|
+
before_validation :normalize_title
|
|
10
|
+
|
|
11
|
+
def normalize_title
|
|
12
|
+
self.title = title&.strip&.downcase
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
form = form_class.new(title: ' HELLO ')
|
|
17
|
+
form.valid?
|
|
18
|
+
|
|
19
|
+
expect(form.title).to eq('hello')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'runs after_validation callback' do
|
|
23
|
+
callback_ran = false
|
|
24
|
+
|
|
25
|
+
form_class = Class.new(Voids::Base) do
|
|
26
|
+
attribute :title, :string
|
|
27
|
+
validates :title, presence: true
|
|
28
|
+
|
|
29
|
+
after_validation do
|
|
30
|
+
callback_ran = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_method(:callback_ran=) { |value| callback_ran = value }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
form = form_class.new(title: 'test')
|
|
37
|
+
form.valid?
|
|
38
|
+
|
|
39
|
+
expect(callback_ran).to be(true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'runs around_validation callback' do
|
|
43
|
+
form_class = Class.new(Voids::Base) do
|
|
44
|
+
attribute :title, :string
|
|
45
|
+
attribute :validated, :boolean, default: false
|
|
46
|
+
|
|
47
|
+
around_validation :mark_validated
|
|
48
|
+
|
|
49
|
+
def mark_validated
|
|
50
|
+
self.validated = true
|
|
51
|
+
yield
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
form = form_class.new(title: 'test')
|
|
56
|
+
form.valid?
|
|
57
|
+
|
|
58
|
+
expect(form.validated).to be(true)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Custom primary key for nested forms' do
|
|
6
|
+
before(:all) do
|
|
7
|
+
class CustomPrimaryKeyImageForm < Voids::Base
|
|
8
|
+
attribute :uuid, :string
|
|
9
|
+
attribute :url, :string
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CustomPrimaryKeyPostForm < Voids::Base
|
|
13
|
+
attribute :title, :string
|
|
14
|
+
has_many :images, class_name: 'CustomPrimaryKeyImageForm', primary_key: :uuid
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after(:all) do
|
|
19
|
+
Object.send(:remove_const, :CustomPrimaryKeyImageForm)
|
|
20
|
+
Object.send(:remove_const, :CustomPrimaryKeyPostForm)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe 'has_many with custom primary_key' do
|
|
24
|
+
it 'uses custom primary_key to find existing records' do
|
|
25
|
+
form = CustomPrimaryKeyPostForm.new
|
|
26
|
+
form.images.new(uuid: 'abc-123', url: 'original.jpg')
|
|
27
|
+
|
|
28
|
+
form.images_attributes = [
|
|
29
|
+
{ uuid: 'abc-123', url: 'updated.jpg' }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
expect(form.images.count).to eq(1)
|
|
33
|
+
expect(form.images.first.url).to eq('updated.jpg')
|
|
34
|
+
expect(form.images.first.uuid).to eq('abc-123')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'creates new record when primary_key value not found' do
|
|
38
|
+
form = CustomPrimaryKeyPostForm.new
|
|
39
|
+
form.images.new(uuid: 'abc-123', url: 'original.jpg')
|
|
40
|
+
|
|
41
|
+
form.images_attributes = [
|
|
42
|
+
{ uuid: 'def-456', url: 'new.jpg' }
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
expect(form.images.count).to eq(2)
|
|
46
|
+
expect(form.images.map(&:uuid)).to contain_exactly('abc-123', 'def-456')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'excludes primary_key from updated attributes' do
|
|
50
|
+
form = CustomPrimaryKeyPostForm.new
|
|
51
|
+
form.images.new(uuid: 'abc-123', url: 'original.jpg')
|
|
52
|
+
|
|
53
|
+
form.images_attributes = [
|
|
54
|
+
{ uuid: 'abc-123', url: 'updated.jpg' }
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
image = form.images.first
|
|
58
|
+
expect(image.uuid).to eq('abc-123')
|
|
59
|
+
expect(image.url).to eq('updated.jpg')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe 'has_many with custom primary_key and allow_destroy' do
|
|
64
|
+
before(:all) do
|
|
65
|
+
class DestroyableImageForm < Voids::Base
|
|
66
|
+
attribute :uuid, :string
|
|
67
|
+
attribute :url, :string
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class DestroyablePostForm < Voids::Base
|
|
71
|
+
attribute :title, :string
|
|
72
|
+
has_many :images, class_name: 'DestroyableImageForm', primary_key: :uuid, allow_destroy: true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
after(:all) do
|
|
77
|
+
Object.send(:remove_const, :DestroyableImageForm)
|
|
78
|
+
Object.send(:remove_const, :DestroyablePostForm)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'marks record for destruction using custom primary_key' do
|
|
82
|
+
form = DestroyablePostForm.new
|
|
83
|
+
form.images.new(uuid: 'abc-123', url: 'original.jpg')
|
|
84
|
+
|
|
85
|
+
form.images_attributes = [
|
|
86
|
+
{ uuid: 'abc-123', _destroy: true }
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
expect(form.images.first.marked_for_destruction?).to be true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe 'has_many defaults to :id when no primary_key specified' do
|
|
94
|
+
before(:all) do
|
|
95
|
+
class DefaultIdImageForm < Voids::Base
|
|
96
|
+
attribute :id, :integer
|
|
97
|
+
attribute :url, :string
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class DefaultIdPostForm < Voids::Base
|
|
101
|
+
attribute :title, :string
|
|
102
|
+
has_many :images, class_name: 'DefaultIdImageForm'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
after(:all) do
|
|
107
|
+
Object.send(:remove_const, :DefaultIdImageForm)
|
|
108
|
+
Object.send(:remove_const, :DefaultIdPostForm)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'uses id as default primary_key' do
|
|
112
|
+
form = DefaultIdPostForm.new
|
|
113
|
+
form.images.new(id: 1, url: 'original.jpg')
|
|
114
|
+
|
|
115
|
+
form.images_attributes = [
|
|
116
|
+
{ id: 1, url: 'updated.jpg' }
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
expect(form.images.count).to eq(1)
|
|
120
|
+
expect(form.images.first.url).to eq('updated.jpg')
|
|
121
|
+
expect(form.images.first.id).to eq(1)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe 'AssociationProxy#find_by' do
|
|
126
|
+
before(:all) do
|
|
127
|
+
class ProxyTestForm < Voids::Base
|
|
128
|
+
attribute :id, :integer
|
|
129
|
+
attribute :uuid, :string
|
|
130
|
+
attribute :url, :string
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
after(:all) do
|
|
135
|
+
Object.send(:remove_const, :ProxyTestForm)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'finds record by custom attribute' do
|
|
139
|
+
proxy = Voids::AssociationProxy.new('ProxyTestForm')
|
|
140
|
+
image = ProxyTestForm.new(uuid: 'abc-123', url: 'test.jpg')
|
|
141
|
+
proxy.push(image)
|
|
142
|
+
|
|
143
|
+
found = proxy.find_by(:uuid, 'abc-123')
|
|
144
|
+
|
|
145
|
+
expect(found).to eq(image)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'returns nil when attribute value not found' do
|
|
149
|
+
proxy = Voids::AssociationProxy.new('ProxyTestForm')
|
|
150
|
+
image = ProxyTestForm.new(uuid: 'abc-123', url: 'test.jpg')
|
|
151
|
+
proxy.push(image)
|
|
152
|
+
|
|
153
|
+
found = proxy.find_by(:uuid, 'not-found')
|
|
154
|
+
|
|
155
|
+
expect(found).to be_nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it 'uses id cache when finding by id' do
|
|
159
|
+
proxy = Voids::AssociationProxy.new('ProxyTestForm')
|
|
160
|
+
image = ProxyTestForm.new(id: 1, url: 'test.jpg')
|
|
161
|
+
proxy.push(image)
|
|
162
|
+
|
|
163
|
+
found = proxy.find_by(:id, 1)
|
|
164
|
+
|
|
165
|
+
expect(found).to eq(image)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'Dirty tracking' do
|
|
6
|
+
it 'tracks attribute changes' do
|
|
7
|
+
form_class = Class.new(Voids::Base) do
|
|
8
|
+
attribute :title, :string
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
form = form_class.new(title: 'original')
|
|
12
|
+
form.title = 'changed'
|
|
13
|
+
|
|
14
|
+
expect(form.title_changed?).to be(true)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'tracks previous value' do
|
|
18
|
+
form_class = Class.new(Voids::Base) do
|
|
19
|
+
attribute :title, :string
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
form = form_class.new(title: 'original')
|
|
23
|
+
form.title = 'changed'
|
|
24
|
+
|
|
25
|
+
expect(form.title_was).to eq('original')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'provides changes hash' do
|
|
29
|
+
form_class = Class.new(Voids::Base) do
|
|
30
|
+
attribute :title, :string
|
|
31
|
+
attribute :content, :string
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
form = form_class.new(title: 'original', content: 'original content')
|
|
35
|
+
form.title = 'changed'
|
|
36
|
+
|
|
37
|
+
expect(form.changes).to eq('title' => %w[original changed])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'clears changes after assign_attributes' do
|
|
41
|
+
form_class = Class.new(Voids::Base) do
|
|
42
|
+
attribute :title, :string
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
form = form_class.new(title: 'original')
|
|
46
|
+
form.title = 'changed'
|
|
47
|
+
form.assign_attributes(title: 'new')
|
|
48
|
+
|
|
49
|
+
expect(form.changed?).to be(false)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'detects if any attributes changed' do
|
|
53
|
+
form_class = Class.new(Voids::Base) do
|
|
54
|
+
attribute :title, :string
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
form = form_class.new(title: 'original')
|
|
58
|
+
|
|
59
|
+
expect(form.changed?).to be(false)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe 'I18n support' do
|
|
6
|
+
it 'provides human_attribute_name' do
|
|
7
|
+
form_class = Class.new(Voids::Base) do
|
|
8
|
+
attribute :title, :string
|
|
9
|
+
end
|
|
10
|
+
stub_const('PostForm', form_class)
|
|
11
|
+
|
|
12
|
+
expect(PostForm.human_attribute_name(:title)).to eq('Title')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'uses model_name for translation lookup' do
|
|
16
|
+
form_class = Class.new(Voids::Base) do
|
|
17
|
+
attribute :title, :string
|
|
18
|
+
end
|
|
19
|
+
stub_const('PostForm', form_class)
|
|
20
|
+
|
|
21
|
+
expect(PostForm.model_name.i18n_key).to eq(:post)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'supports custom translations via model_name_for' do
|
|
25
|
+
form_class = Class.new(Voids::Base) do
|
|
26
|
+
model_name_for :article
|
|
27
|
+
attribute :title, :string
|
|
28
|
+
end
|
|
29
|
+
stub_const('AdminPostForm', form_class)
|
|
30
|
+
|
|
31
|
+
expect(AdminPostForm.model_name.i18n_key).to eq(:article)
|
|
32
|
+
end
|
|
33
|
+
end
|