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
data/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# Voids
|
|
2
|
+
|
|
3
|
+
_fill it in_.
|
|
4
|
+
|
|
5
|
+
Voids is a form object pattern that works with Rails form helpers and validations without requiring database persistence.
|
|
6
|
+
|
|
7
|
+
Most forms don't map to a single database table; some don't map to the database at all. This gem provides form objects that implement the ActiveModel interface—associations, validations, nested attributes, dirty tracking—without requiring ActiveRecord.
|
|
8
|
+
|
|
9
|
+
Built on ActiveModel, so the Rails conventions you already know work here. Use the same validations, callbacks, attribute types, and form helpers. Forms integrate with `form_with` and `fields_for` without configuration. Error handling, i18n, dirty tracking, and model naming all follow standard Rails patterns.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
You've done this before:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'voids'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic form with attributes
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class PostForm < Voids::Base
|
|
26
|
+
attribute :title, :string
|
|
27
|
+
attribute :content, :string
|
|
28
|
+
attribute :created_at, :datetime, default: -> { Time.current }
|
|
29
|
+
|
|
30
|
+
validates :title, presence: true
|
|
31
|
+
validates :content, presence: true
|
|
32
|
+
validates :created_at, presence: true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
form = PostForm.new(title: "hello", content: "world")
|
|
36
|
+
form.valid? # true
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Inheriting attributes from models
|
|
40
|
+
|
|
41
|
+
Pull attribute definitions from existing models instead of manually defining each one.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rails g model Post title:string content:text
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
class Post < ApplicationRecord; end
|
|
49
|
+
|
|
50
|
+
class PostForm < Voids::Base
|
|
51
|
+
inherit_attributes_from Post, except: [:created_at, :updated_at]
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `only:` to include specific attributes:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class PostForm < Voids::Base
|
|
59
|
+
inherit_attributes_from Post, only: [:title, :content]
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Inherited attributes work with `from_model` and preserve the model's attribute types.
|
|
64
|
+
|
|
65
|
+
### Inheriting validations from models
|
|
66
|
+
|
|
67
|
+
Copy validation rules from existing models.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
class Post < ApplicationRecord
|
|
71
|
+
validates :title, presence: true, length: { minimum: 3 }
|
|
72
|
+
validates :content, presence: true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class PostForm < Voids::Base
|
|
76
|
+
inherit_attributes_from Post, only: [:title, :content]
|
|
77
|
+
inherit_validations_from Post, only: [:title]
|
|
78
|
+
|
|
79
|
+
validates :content, presence: true, length: { minimum: 50 }
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use `only:` and `except:` to control which validations are inherited.
|
|
84
|
+
|
|
85
|
+
#### What doesn't copy
|
|
86
|
+
|
|
87
|
+
Some validators are skipped because they don't translate cleanly to form objects:
|
|
88
|
+
|
|
89
|
+
**Proc/lambda conditionals** - Validators with `if: -> { ... }` or `unless: -> { ... }` are skipped. The closure captures the source class context and won't work correctly on the form.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
validates :content, presence: true, if: -> { published? }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Symbol conditionals** work fine, but the form must define the method:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
validates :content, presence: true, if: :published?
|
|
99
|
+
|
|
100
|
+
def published?
|
|
101
|
+
status == "published"
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Association validators** - `validates_associated` references ActiveRecord associations that don't exist on forms. These are always skipped.
|
|
106
|
+
|
|
107
|
+
**Custom validators** work if they're `EachValidator` subclasses. Validators that call methods specific to the source model will fail at runtime.
|
|
108
|
+
|
|
109
|
+
**Missing attributes** - If you inherit a validation for an attribute that doesn't exist on the form, it will raise at runtime. Use `inherit_attributes_from` first or define the attribute manually.
|
|
110
|
+
|
|
111
|
+
### Associations
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class ImageForm < Voids::Base
|
|
115
|
+
attribute :url, :string
|
|
116
|
+
validates :url, presence: true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class PostForm < Voids::Base
|
|
120
|
+
has_one :cover_photo # defaults to CoverPhotoForm
|
|
121
|
+
has_many :images # defaults to ImageForm
|
|
122
|
+
|
|
123
|
+
attribute :title, :string
|
|
124
|
+
validates :title, presence: true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
form = PostForm.new
|
|
128
|
+
form.images.new(url: "https://example.com/image.jpg")
|
|
129
|
+
form.images.count # 1
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Nested attributes from params
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
params = {
|
|
136
|
+
title: "hello",
|
|
137
|
+
cover_photo_attributes: { url: "https://example.com/cover.jpg" },
|
|
138
|
+
images_attributes: [
|
|
139
|
+
{ url: "https://example.com/1.jpg" },
|
|
140
|
+
{ url: "https://example.com/2.jpg" }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
form = PostForm.new(params)
|
|
145
|
+
form.cover_photo.url # "https://example.com/cover.jpg"
|
|
146
|
+
form.images.count # 2
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Load from model
|
|
150
|
+
|
|
151
|
+
Class method creates a new instance:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
post = Post.find(1)
|
|
155
|
+
form = PostForm.from_model(post)
|
|
156
|
+
|
|
157
|
+
form.title # value from post
|
|
158
|
+
form.cover_photo.url # value from post.cover_photo
|
|
159
|
+
form.images.count # post.images.count
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Instance method for existing forms:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
form = PostForm.new
|
|
166
|
+
form.from_model(post)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Validation with nested forms
|
|
170
|
+
|
|
171
|
+
Validations automatically cascade to nested forms:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
form = PostForm.new(title: "hello")
|
|
175
|
+
form.images.new(url: nil) # invalid image
|
|
176
|
+
|
|
177
|
+
form.valid? # false
|
|
178
|
+
form.errors.full_messages # includes nested form errors
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Extracting attributes for persistence
|
|
182
|
+
|
|
183
|
+
Use `model_attributes` for the form's own attributes:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
form = PostForm.new(title: "test")
|
|
187
|
+
form.model_attributes # => { "title" => "test", "content" => nil }
|
|
188
|
+
|
|
189
|
+
Post.create!(form.model_attributes)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use `attributes` for all attributes including nested:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
form = PostForm.new(
|
|
196
|
+
title: "test",
|
|
197
|
+
images_attributes: [{ url: "image.jpg" }]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
form.attributes
|
|
201
|
+
# => {
|
|
202
|
+
# "title" => "test",
|
|
203
|
+
# "content" => nil,
|
|
204
|
+
# "images_attributes" => [{ "url" => "image.jpg" }]
|
|
205
|
+
# }
|
|
206
|
+
|
|
207
|
+
Post.create!(form.attributes) # works with accepts_nested_attributes_for
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### ID tracking in nested forms
|
|
211
|
+
|
|
212
|
+
When editing existing records, nested forms update by id:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
post = Post.find(1) # has images with id: 1, 2, 3
|
|
216
|
+
form = PostForm.from_model(post)
|
|
217
|
+
|
|
218
|
+
form.images_attributes = [
|
|
219
|
+
{ id: 1, url: "updated.jpg" }, # updates existing
|
|
220
|
+
{ url: "new.jpg" } # creates new
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
form.images.count # 4 (3 original + 1 new)
|
|
224
|
+
form.images[0].url # "updated.jpg"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Use `primary_key` option for non-id identifiers:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class ImageForm < Voids::Base
|
|
231
|
+
attribute :uuid, :string
|
|
232
|
+
attribute :url, :string
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
class PostForm < Voids::Base
|
|
236
|
+
has_many :images, primary_key: :uuid
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
form = PostForm.new
|
|
240
|
+
form.images.new(uuid: "abc-123", url: "original.jpg")
|
|
241
|
+
|
|
242
|
+
form.images_attributes = [
|
|
243
|
+
{ uuid: "abc-123", url: "updated.jpg" } # updates by uuid
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
form.images.first.url # "updated.jpg"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Works with `has_one` and `has_many`. Defaults to `:id`.
|
|
250
|
+
|
|
251
|
+
### Destroying nested forms
|
|
252
|
+
|
|
253
|
+
Mark nested records for deletion with `allow_destroy: true`:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
class PostForm < Voids::Base
|
|
257
|
+
has_many :images, allow_destroy: true
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
post = Post.find(1) # has images with id: 1, 2, 3
|
|
261
|
+
form = PostForm.from_model(post)
|
|
262
|
+
|
|
263
|
+
form.images_attributes = [
|
|
264
|
+
{ id: 1, url: "updated.jpg" }, # updates existing
|
|
265
|
+
{ id: 2, _destroy: true }, # marks for deletion
|
|
266
|
+
{ url: "new.jpg" } # creates new
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
form.images[1].marked_for_destruction? # true
|
|
270
|
+
form.attributes["images_attributes"][1]["_destroy"] # true
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
When extracting attributes, records marked for destruction include `_destroy: true`. Works with has_one and has_many.
|
|
274
|
+
|
|
275
|
+
### Dirty tracking
|
|
276
|
+
|
|
277
|
+
Track attribute changes:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
form = PostForm.new(title: "original")
|
|
281
|
+
form.title = "changed"
|
|
282
|
+
|
|
283
|
+
form.title_changed? # true
|
|
284
|
+
form.title_was # "original"
|
|
285
|
+
form.changes # { "title" => ["original", "changed"] }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Normalization
|
|
289
|
+
|
|
290
|
+
Normalize attribute values on assignment. Works on Rails 6+, not just 7.1+.
|
|
291
|
+
|
|
292
|
+
Normalization is idempotent—applying it multiple times produces the same result as applying it once.
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
class UserForm < Voids::Base
|
|
296
|
+
attribute :email, :string
|
|
297
|
+
attribute :phone, :string
|
|
298
|
+
|
|
299
|
+
normalizes :email, with: ->(email) { email.strip.downcase }
|
|
300
|
+
normalizes :phone, with: ->(phone) { phone.delete("^0-9").delete_prefix("1") }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
form = UserForm.new(email: " TEST@EXAMPLE.COM\n")
|
|
304
|
+
form.email # "test@example.com"
|
|
305
|
+
|
|
306
|
+
form = UserForm.new(phone: "1-555-123-4567")
|
|
307
|
+
form.phone # "5551234567"
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Normalize multiple attributes with one call:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
class PostForm < Voids::Base
|
|
314
|
+
attribute :title, :string
|
|
315
|
+
attribute :author, :string
|
|
316
|
+
|
|
317
|
+
normalizes :title, :author, with: ->(value) { value.strip }
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
By default, normalization skips nil values. Override with `apply_to_nil: true`:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
normalizes :email, with: ->(email) { email || "default@example.com" }, apply_to_nil: true
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Callbacks
|
|
328
|
+
|
|
329
|
+
Hook into the validation lifecycle:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
class PostForm < Voids::Base
|
|
333
|
+
attribute :title, :string
|
|
334
|
+
|
|
335
|
+
before_validation :normalize_title
|
|
336
|
+
after_validation :log_errors
|
|
337
|
+
|
|
338
|
+
def normalize_title
|
|
339
|
+
self.title = title&.strip&.downcase
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def log_errors
|
|
343
|
+
Rails.logger.error(errors.full_messages) if errors.any?
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Rails form integration
|
|
349
|
+
|
|
350
|
+
#### model naming
|
|
351
|
+
|
|
352
|
+
Form classes automatically strip the "Form" suffix for Rails form helpers:
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
class PostForm < Voids::Base
|
|
356
|
+
attribute :title, :string
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
form = PostForm.new
|
|
360
|
+
form.model_name.param_key # "post"
|
|
361
|
+
# rails generates: <input name="post[title]">
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Override when needed:
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
class AdminArticleForm < Voids::Base
|
|
368
|
+
model_name_for :article
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
form.model_name.param_key # "article"
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### persistence detection
|
|
375
|
+
|
|
376
|
+
Forms automatically detect persistence via the `id` attribute:
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
class PostForm < Voids::Base
|
|
380
|
+
attribute :id, :integer
|
|
381
|
+
attribute :title, :string
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
form = PostForm.new
|
|
385
|
+
form.persisted? # false
|
|
386
|
+
|
|
387
|
+
form = PostForm.new(id: 123)
|
|
388
|
+
form.persisted? # true
|
|
389
|
+
form.to_param # "123"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Override for custom logic:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
class PostForm < Voids::Base
|
|
396
|
+
def persisted?
|
|
397
|
+
# custom logic
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def to_param
|
|
401
|
+
# custom logic
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### i18n
|
|
407
|
+
|
|
408
|
+
Standard ActiveModel translation support:
|
|
409
|
+
|
|
410
|
+
```yaml
|
|
411
|
+
# config/locales/en.yml
|
|
412
|
+
en:
|
|
413
|
+
activemodel:
|
|
414
|
+
attributes:
|
|
415
|
+
post: # uses model_name, not PostForm
|
|
416
|
+
title: "Post Title"
|
|
417
|
+
errors:
|
|
418
|
+
models:
|
|
419
|
+
post:
|
|
420
|
+
attributes:
|
|
421
|
+
title:
|
|
422
|
+
blank: "must be provided"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## License
|
|
426
|
+
|
|
427
|
+
MIT.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'voids'
|
|
4
|
+
|
|
5
|
+
class ImageForm < Voids::Base
|
|
6
|
+
attribute :id, :integer
|
|
7
|
+
attribute :url, :string
|
|
8
|
+
attribute :caption, :string
|
|
9
|
+
|
|
10
|
+
validates :url, presence: true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class PostForm < Voids::Base
|
|
14
|
+
has_many :images
|
|
15
|
+
|
|
16
|
+
attribute :id, :integer
|
|
17
|
+
attribute :title, :string
|
|
18
|
+
attribute :content, :string
|
|
19
|
+
|
|
20
|
+
validates :title, presence: true
|
|
21
|
+
|
|
22
|
+
before_validation :normalize_title
|
|
23
|
+
|
|
24
|
+
def normalize_title
|
|
25
|
+
self.title = title&.strip if title
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
puts '=== dirty tracking ==='
|
|
30
|
+
form = PostForm.new(title: 'original')
|
|
31
|
+
puts "initial: #{form.title}"
|
|
32
|
+
|
|
33
|
+
form.title = 'changed'
|
|
34
|
+
puts "changed?: #{form.title_changed?}"
|
|
35
|
+
puts "was: #{form.title_was}"
|
|
36
|
+
puts "changes: #{form.changes.inspect}"
|
|
37
|
+
|
|
38
|
+
puts "\n=== callbacks ==="
|
|
39
|
+
form = PostForm.new(title: ' HELLO ')
|
|
40
|
+
form.valid?
|
|
41
|
+
puts "normalized title: #{form.title}"
|
|
42
|
+
|
|
43
|
+
puts "\n=== model_attributes (just top-level) ==="
|
|
44
|
+
form = PostForm.new(title: 'test', content: 'content')
|
|
45
|
+
form.images.new(url: 'image.jpg')
|
|
46
|
+
puts form.model_attributes.inspect
|
|
47
|
+
|
|
48
|
+
puts "\n=== attributes (includes nested) ==="
|
|
49
|
+
puts form.attributes.inspect
|
|
50
|
+
|
|
51
|
+
puts "\n=== id tracking in nested forms ==="
|
|
52
|
+
mock_image1 = Struct.new(:id, :url, :caption).new(1, 'original1.jpg', 'first')
|
|
53
|
+
mock_image2 = Struct.new(:id, :url, :caption).new(2, 'original2.jpg', 'second')
|
|
54
|
+
mock_post = Struct.new(:id, :title, :content, :images).new(
|
|
55
|
+
100,
|
|
56
|
+
'my post',
|
|
57
|
+
'my content',
|
|
58
|
+
[mock_image1, mock_image2]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
form = PostForm.from_model(mock_post)
|
|
62
|
+
puts "loaded from model, images count: #{form.images.count}"
|
|
63
|
+
|
|
64
|
+
form.images_attributes = [
|
|
65
|
+
{ id: 1, url: 'updated1.jpg' },
|
|
66
|
+
{ url: 'new.jpg', caption: 'new image' }
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
puts "after update, images count: #{form.images.count}"
|
|
70
|
+
puts "image 1 url: #{form.images[0].url}"
|
|
71
|
+
puts "image 2 url: #{form.images[1].url}"
|
|
72
|
+
puts "image 3 url: #{form.images[2].url}"
|
|
73
|
+
|
|
74
|
+
puts "\n=== using with activerecord ==="
|
|
75
|
+
puts 'for create:'
|
|
76
|
+
puts 'Post.create!(form.attributes)'
|
|
77
|
+
puts form.attributes.inspect
|
|
78
|
+
|
|
79
|
+
puts "\nfor update:"
|
|
80
|
+
puts 'post.update!(form.model_attributes)'
|
|
81
|
+
puts form.model_attributes.inspect
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'voids'
|
|
4
|
+
|
|
5
|
+
class ImageForm < Voids::Base
|
|
6
|
+
attribute :url, :string
|
|
7
|
+
attribute :caption, :string
|
|
8
|
+
|
|
9
|
+
validates :url, presence: true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CoverPhotoForm < Voids::Base
|
|
13
|
+
attribute :url, :string
|
|
14
|
+
|
|
15
|
+
validates :url, presence: true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class PostForm < Voids::Base
|
|
19
|
+
has_one :cover_photo
|
|
20
|
+
has_many :images
|
|
21
|
+
|
|
22
|
+
attribute :title, :string
|
|
23
|
+
attribute :content, :string
|
|
24
|
+
attribute :created_at, :datetime
|
|
25
|
+
|
|
26
|
+
validates :title, presence: true
|
|
27
|
+
validates :content, presence: true
|
|
28
|
+
validates :created_at, presence: true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
MockImage = Struct.new(:url, :caption, keyword_init: true)
|
|
32
|
+
MockCoverPhoto = Struct.new(:url, keyword_init: true)
|
|
33
|
+
MockPost = Struct.new(:title, :content, :created_at, :cover_photo, :images, keyword_init: true)
|
|
34
|
+
|
|
35
|
+
mock_post = MockPost.new(
|
|
36
|
+
title: 'model title',
|
|
37
|
+
content: 'model content',
|
|
38
|
+
created_at: Time.now,
|
|
39
|
+
cover_photo: MockCoverPhoto.new(url: 'https://example.com/model-cover.jpg'),
|
|
40
|
+
images: [
|
|
41
|
+
MockImage.new(url: 'https://example.com/model-1.jpg', caption: 'model image 1'),
|
|
42
|
+
MockImage.new(url: 'https://example.com/model-2.jpg', caption: 'model image 2')
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
form = PostForm.from_model(mock_post)
|
|
47
|
+
|
|
48
|
+
puts "form valid: #{form.valid?}"
|
|
49
|
+
puts "title: #{form.title}"
|
|
50
|
+
puts "content: #{form.content}"
|
|
51
|
+
puts "cover photo url: #{form.cover_photo.url}"
|
|
52
|
+
puts "images count: #{form.images.count}"
|
|
53
|
+
puts "first image url: #{form.images[0].url}"
|
|
54
|
+
puts "first image caption: #{form.images[0].caption}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'voids'
|
|
4
|
+
|
|
5
|
+
class PostForm < Voids::Base
|
|
6
|
+
attribute :id, :integer
|
|
7
|
+
attribute :title, :string
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class AdminArticleForm < Voids::Base
|
|
11
|
+
model_name_for :article
|
|
12
|
+
|
|
13
|
+
attribute :id, :integer
|
|
14
|
+
attribute :title, :string
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
form1 = PostForm.new
|
|
18
|
+
puts "postform model_name: #{form1.model_name}"
|
|
19
|
+
puts "postform model_name.param_key: #{form1.model_name.param_key}"
|
|
20
|
+
puts "postform persisted?: #{form1.persisted?}"
|
|
21
|
+
puts "postform to_param: #{form1.to_param.inspect}"
|
|
22
|
+
|
|
23
|
+
form2 = PostForm.new(id: 123, title: 'hello')
|
|
24
|
+
puts "\npostform with id model_name: #{form2.model_name}"
|
|
25
|
+
puts "postform with id persisted?: #{form2.persisted?}"
|
|
26
|
+
puts "postform with id to_param: #{form2.to_param}"
|
|
27
|
+
puts "postform with id to_key: #{form2.to_key.inspect}"
|
|
28
|
+
|
|
29
|
+
form3 = AdminArticleForm.new
|
|
30
|
+
puts "\nadminarticleform model_name: #{form3.model_name}"
|
|
31
|
+
puts "adminarticleform model_name.param_key: #{form3.model_name.param_key}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'voids'
|
|
4
|
+
|
|
5
|
+
class UserForm < Voids::Base
|
|
6
|
+
attribute :email, :string
|
|
7
|
+
attribute :phone, :string
|
|
8
|
+
attribute :username, :string
|
|
9
|
+
|
|
10
|
+
normalizes :email, with: ->(email) { email.strip.downcase }
|
|
11
|
+
normalizes :phone, with: ->(phone) { phone.delete('^0-9').delete_prefix('1') }
|
|
12
|
+
normalizes :username, with: :downcase
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
puts 'email normalization'
|
|
16
|
+
form = UserForm.new(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
|
|
17
|
+
puts "input: ' CRUISE-CONTROL@EXAMPLE.COM\\n'"
|
|
18
|
+
puts "normalized: #{form.email.inspect}"
|
|
19
|
+
puts
|
|
20
|
+
|
|
21
|
+
puts 'phone normalization'
|
|
22
|
+
form = UserForm.new(phone: '1-555-123-4567')
|
|
23
|
+
puts "input: '1-555-123-4567'"
|
|
24
|
+
puts "normalized: #{form.phone.inspect}"
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
puts 'symbol normalizer'
|
|
28
|
+
form = UserForm.new(username: 'JohnDoe')
|
|
29
|
+
puts "input: 'JohnDoe'"
|
|
30
|
+
puts "normalized: #{form.username.inspect}"
|
|
31
|
+
puts
|
|
32
|
+
|
|
33
|
+
puts 'idempotency test'
|
|
34
|
+
form = UserForm.new(email: ' TEST@EXAMPLE.COM ')
|
|
35
|
+
puts "initial: #{form.email.inspect}"
|
|
36
|
+
form.email = form.email
|
|
37
|
+
puts "after reassignment: #{form.email.inspect}"
|
|
38
|
+
form.email = form.email
|
|
39
|
+
puts "after second reassignment: #{form.email.inspect}"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'voids'
|
|
4
|
+
|
|
5
|
+
class ImageForm < Voids::Base
|
|
6
|
+
attribute :url, :string
|
|
7
|
+
attribute :caption, :string
|
|
8
|
+
|
|
9
|
+
validates :url, presence: true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CoverPhotoForm < Voids::Base
|
|
13
|
+
attribute :url, :string
|
|
14
|
+
|
|
15
|
+
validates :url, presence: true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class PostForm < Voids::Base
|
|
19
|
+
has_one :cover_photo
|
|
20
|
+
has_many :images
|
|
21
|
+
|
|
22
|
+
attribute :title, :string
|
|
23
|
+
attribute :content, :string
|
|
24
|
+
attribute :created_at, :datetime, default: -> { Time.current }
|
|
25
|
+
|
|
26
|
+
validates :title, presence: true
|
|
27
|
+
validates :content, presence: true
|
|
28
|
+
validates :created_at, presence: true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
form = PostForm.new
|
|
32
|
+
puts "empty form valid: #{form.valid?}"
|
|
33
|
+
|
|
34
|
+
form = PostForm.new(
|
|
35
|
+
title: 'hello world',
|
|
36
|
+
content: 'this is content',
|
|
37
|
+
created_at: Time.now,
|
|
38
|
+
cover_photo_attributes: { url: 'https://example.com/cover.jpg' },
|
|
39
|
+
images_attributes: [
|
|
40
|
+
{ url: 'https://example.com/1.jpg', caption: 'first' },
|
|
41
|
+
{ url: 'https://example.com/2.jpg', caption: 'second' }
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
puts "form with attributes valid: #{form.valid?}"
|
|
46
|
+
puts "title: #{form.title}"
|
|
47
|
+
puts "cover photo url: #{form.cover_photo.url}"
|
|
48
|
+
puts "images count: #{form.images.count}"
|
|
49
|
+
|
|
50
|
+
form = PostForm.new
|
|
51
|
+
form.images.new(url: 'https://example.com/new.jpg', caption: 'new image')
|
|
52
|
+
puts "form with new image count: #{form.images.count}"
|