reform 1.2.0.beta2 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGES.md +53 -7
- data/LICENSE.txt +1 -1
- data/README.md +48 -8
- data/database.sqlite3 +0 -0
- data/lib/reform/contract.rb +58 -4
- data/lib/reform/contract/setup.rb +16 -22
- data/lib/reform/contract/validate.rb +18 -21
- data/lib/reform/form/active_model.rb +6 -8
- data/lib/reform/form/json.rb +1 -1
- data/lib/reform/form/save.rb +29 -59
- data/lib/reform/form/sync.rb +52 -70
- data/lib/reform/form/validate.rb +37 -44
- data/lib/reform/representer.rb +6 -10
- data/lib/reform/version.rb +1 -1
- data/test/active_record_test.rb +68 -0
- data/test/benchmarking.rb +26 -0
- data/test/contract_test.rb +40 -0
- data/test/custom_validation_test.rb +1 -1
- data/test/empty_test.rb +31 -3
- data/test/form_composition_test.rb +1 -1
- data/test/from_test.rb +150 -0
- data/test/model_validations_test.rb +2 -2
- data/test/nested_form_test.rb +4 -4
- data/test/read_only_test.rb +0 -25
- data/test/readable_test.rb +32 -0
- data/test/reform_test.rb +0 -21
- data/test/virtual_test.rb +26 -0
- data/test/writeable_test.rb +30 -0
- metadata +14 -6
- data/test/as_test.rb +0 -75
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8d0dc76658a132e15bea8612f35e7bfa77fb7b8
|
4
|
+
data.tar.gz: 31226f61d63c42b0a641b5592eb9714cb29bcff2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cde409c7fa9dc9054d05e2dafb7cf672abf8585d85b97f05f8f0d37b0d6c0de25e554775fb7a8a5eec57567977ba8ff03cdcb2276190c964e5f62c31259ce027
|
7
|
+
data.tar.gz: 70ac7b9421a13e0936ba9e149f611ad9f540b071f1f557d474a191c9c1d4a7e2a8fc126a9510a7fed65b1ef70078033ea4e8f34a6d9d2bc3dd9b74fbc61bd1c3
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
## 1.2.1
|
2
|
+
|
3
|
+
* Fixed a nasty bug where `ActiveModel` forms with form builder support wouldn't deserialize properly. A million Thanks to @karolsarnacki for finding this and providing an exemplary failing test. <3
|
4
|
+
|
1
5
|
## 1.2.0
|
2
6
|
|
3
7
|
### Breakage
|
@@ -11,16 +15,29 @@
|
|
11
15
|
|
12
16
|
Including this module will add `#column_for_attribute` and other methods need by form builders to automatically guess the type of a property.
|
13
17
|
|
18
|
+
* `Form#save` no longer passed `self` to the block. You've been warned long enough. ;)
|
19
|
+
|
14
20
|
### Changes
|
15
21
|
|
22
|
+
* Renamed `:as` to `:from` to be in line with Representable/Roar, Disposable and Cells. Thanks to @bethesque for pushing this.
|
23
|
+
* `:empty` is now `:virtual` and `:virtual` is `writeable: false`. It was too confusing and sucked. Thanks to @bethesque, again, for her moral assistance.
|
16
24
|
* `Form#save` with `Composition` now returns true only if all composite models saved.
|
17
25
|
* `Form::copy_validations_from` allows copying custom validators now.
|
18
26
|
* New call style for `::properties`. Instead of an array, it's now `properties :title, :genre`.
|
19
27
|
* All options are evaluated with `pass_options: true`.
|
28
|
+
* All transforming representers are now created and stored on class level, resulting in simpler code and a 85% speed-up.
|
20
29
|
|
21
30
|
### New Stuff!!!
|
22
31
|
|
23
|
-
*
|
32
|
+
* In `#validate`, you can ignore properties now using `:skip_if`.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
property :hit, skip_if: lambda { |fragment, *| fragment["title"].blank? }
|
36
|
+
```
|
37
|
+
|
38
|
+
This works for both properties and nested forms. The property will simply be ignored when deserializing, as if it had never been in the incoming hash/document.
|
39
|
+
|
40
|
+
For nested properties you can use `:skip_if: :all_blank` as a macro to ignore a nested form if all values are blank.
|
24
41
|
* You can now specify validations right in the `::property` call.
|
25
42
|
|
26
43
|
```ruby
|
@@ -28,19 +45,48 @@
|
|
28
45
|
```
|
29
46
|
|
30
47
|
Thanks to @zubin for this brillant idea!
|
31
|
-
|
32
|
-
*
|
33
|
-
*
|
34
|
-
*
|
35
|
-
|
36
|
-
|
48
|
+
|
49
|
+
* Reform now tracks which attributes have changed after `#validate`. You can check that using `form.changed?(:title)`.
|
50
|
+
* When including `Sync::SkipUnchanged`, the form won't try to assign unchanged values anymore in `#sync`. This is extremely helpful when handling file uploads and the like.
|
51
|
+
* Both `#sync` and `#save` can be configured dynamically now.
|
52
|
+
|
53
|
+
When syncing, you can run a lambda per property.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
property :title, sync: lambda { |value, options| model.set_title(value) }
|
57
|
+
```
|
58
|
+
|
59
|
+
This won't run Reform's built-in sync for this property.
|
60
|
+
|
61
|
+
You can also provide the sync lambda at run-time.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
form.sync(title: lambda { |value, options| form.model.title = "HOT: #{value}" })
|
65
|
+
```
|
66
|
+
|
67
|
+
This block is run in the caller's context allowing you to access environment variables.
|
68
|
+
|
69
|
+
Note that the dynamic sync happens _before_ save, so the model id may unavailable.
|
70
|
+
|
71
|
+
You can do the same for saving.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
form.save(title: lambda { |value, options| form.model.title = "#{form.model.id} --> #{value}" })
|
75
|
+
```
|
76
|
+
Again, this block is run in the caller's context.
|
77
|
+
|
78
|
+
The two features are an excellent way to handle file uploads without ActiveRecord's horrible callbacks.
|
79
|
+
|
80
|
+
* Adding generic `:base` errors now works. Thanks to @bethesque.
|
37
81
|
|
38
82
|
```ruby
|
39
83
|
errors.add(:base, "You are too awesome!")
|
40
84
|
```
|
41
85
|
|
42
86
|
This will prefix the error with `:base`.
|
87
|
+
* Need your form to parse JSON? Include `Reform::Form::JSON`, the `#validate` method now expects a JSON string and will deserialize and populate the form from the JSON document.
|
43
88
|
* Added `Form::schema` to generate a pure representer from the form's representer.
|
89
|
+
* Added `:readable` and `:writeable` option which allow to skip reading or writing to the model when `false`.
|
44
90
|
|
45
91
|
## 1.1.1
|
46
92
|
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -98,11 +98,11 @@ class SongForm < Reform::Form
|
|
98
98
|
|
99
99
|
Internally, this form will call `song.title` to populate the title field.
|
100
100
|
|
101
|
-
If you, for whatever reasons, want to use a different public name, use `:
|
101
|
+
If you, for whatever reasons, want to use a different public name, use `:from`.
|
102
102
|
|
103
103
|
```ruby
|
104
104
|
class SongForm < Reform::Form
|
105
|
-
property :name,
|
105
|
+
property :name, from: :title
|
106
106
|
```
|
107
107
|
|
108
108
|
This will still call `song.title` but expose the attribute as `name`.
|
@@ -633,17 +633,17 @@ As with the built-in coercion, this setter is only called in `#validate`.
|
|
633
633
|
Virtual fields come in handy when there's no direct mapping to a model attribute or when you plan on displaying but not processing a value.
|
634
634
|
|
635
635
|
|
636
|
-
###
|
636
|
+
### Virtual Fields
|
637
637
|
|
638
|
-
Often, fields like `password_confirmation`
|
638
|
+
Often, fields like `password_confirmation` should neither be read from nor written back to the model. Reform comes with the `:virtual` option to handle that case.
|
639
639
|
|
640
640
|
```ruby
|
641
641
|
class PasswordForm < Reform::Form
|
642
642
|
property :password
|
643
|
-
property :password_confirmation, :
|
643
|
+
property :password_confirmation, virtual: true
|
644
644
|
```
|
645
645
|
|
646
|
-
Here, the model won't be queried for a `password_confirmation` field when creating and rendering the form.
|
646
|
+
Here, the model won't be queried for a `password_confirmation` field when creating and rendering the form. When saving the form, the input value is not written to the decorated model. It is only readable in validations and when saving the form manually.
|
647
647
|
|
648
648
|
```ruby
|
649
649
|
form.validate("password" => "123", "password_confirmation" => "321")
|
@@ -660,11 +660,11 @@ form.save do |nested|
|
|
660
660
|
|
661
661
|
### Read-Only Fields
|
662
662
|
|
663
|
-
|
663
|
+
When you want to show a value but skip processing it after submission the `:writeable` option is your friend.
|
664
664
|
|
665
665
|
```ruby
|
666
666
|
class ProfileForm < Reform::Form
|
667
|
-
property :country, :
|
667
|
+
property :country, :writeable => false
|
668
668
|
```
|
669
669
|
|
670
670
|
This time reform will query the model for the value by calling `model.country`.
|
@@ -678,6 +678,14 @@ form.save do |nested|
|
|
678
678
|
nested[:country] #=> "Australia"
|
679
679
|
```
|
680
680
|
|
681
|
+
### Write-Only Fields
|
682
|
+
|
683
|
+
A third alternative is to hide a field's value but write it to the database when syncing. This can be achieved using the `:readable` option.
|
684
|
+
|
685
|
+
```ruby
|
686
|
+
property :credit_card_number, :readable => false
|
687
|
+
```
|
688
|
+
|
681
689
|
## Validations From Models
|
682
690
|
|
683
691
|
Sometimes when you still keep validations in your models (which you shouldn't) copying them to a form might not feel right. In that case, you can let Reform automatically copy them.
|
@@ -869,6 +877,38 @@ form.validate("title" => "Just Kiddin'")
|
|
869
877
|
form.changed?(:title) #=> true
|
870
878
|
```
|
871
879
|
|
880
|
+
When including `Sync::SkipUnchanged`, the form won't assign unchanged values anymore in `#sync`.
|
881
|
+
|
882
|
+
|
883
|
+
## Dynamically Syncing And Saving Properties
|
884
|
+
|
885
|
+
Both `#sync` and `#save` can be configured to run a dynamical lambda per property.
|
886
|
+
|
887
|
+
The `sync:` option allows to statically add a lambda to a property.
|
888
|
+
|
889
|
+
```ruby
|
890
|
+
property :title, sync: lambda { |value, options| model.set_title(value) }
|
891
|
+
```
|
892
|
+
|
893
|
+
Instead of running Reform's built-in sync for this property the block is run.
|
894
|
+
|
895
|
+
You can also provide the sync lambda at run-time.
|
896
|
+
|
897
|
+
```ruby
|
898
|
+
form.sync(title: lambda { |value, options| form.model.title = "HOT: #{value}" })
|
899
|
+
```
|
900
|
+
|
901
|
+
This block is run in the caller's context allowing you to access environment variables. Note that the dynamic sync happens _before_ save, so the model id may unavailable.
|
902
|
+
|
903
|
+
You can do the same for saving.
|
904
|
+
|
905
|
+
```ruby
|
906
|
+
form.save(title: lambda { |value, options| form.model.title = "#{form.model.id} --> #{value}" })
|
907
|
+
```
|
908
|
+
Again, this block is run in the caller's context.
|
909
|
+
|
910
|
+
The two features are an excellent way to handle file uploads without ActiveRecord's horrible callbacks.
|
911
|
+
|
872
912
|
|
873
913
|
## Undocumented Features
|
874
914
|
|
data/database.sqlite3
CHANGED
Binary file
|
data/lib/reform/contract.rb
CHANGED
@@ -29,12 +29,27 @@ module Reform
|
|
29
29
|
|
30
30
|
module PropertyMethods
|
31
31
|
def property(name, options={}, &block)
|
32
|
-
options
|
32
|
+
deprecate_as!(options)
|
33
|
+
options[:private_name] = options.delete(:from)
|
33
34
|
options[:coercion_type] = options.delete(:type)
|
34
35
|
options[:features] ||= []
|
35
36
|
options[:features] += features.keys if block_given?
|
36
37
|
options[:pass_options] = true
|
37
|
-
|
38
|
+
|
39
|
+
# readable and writeable is true as it's not == false
|
40
|
+
|
41
|
+
if reform_2_0
|
42
|
+
if options.delete(:virtual)
|
43
|
+
options[:_readable] = false
|
44
|
+
options[:_writeable] = false
|
45
|
+
else
|
46
|
+
options[:_readable] = options.delete(:readable)
|
47
|
+
options[:_writeable] = options.delete(:writeable)
|
48
|
+
end
|
49
|
+
|
50
|
+
else # TODO: remove me in 2.0.
|
51
|
+
deprecate_virtual_and_empty!(options)
|
52
|
+
end
|
38
53
|
|
39
54
|
validates(name, options.delete(:validates).dup) if options[:validates]
|
40
55
|
|
@@ -98,10 +113,24 @@ module Reform
|
|
98
113
|
|
99
114
|
require 'reform/contract/setup'
|
100
115
|
include Setup
|
116
|
+
|
117
|
+
def self.representers # keeps all transformation representers for one class.
|
118
|
+
@representers ||= {}
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.representer(name=nil, options={}, &block)
|
122
|
+
return representer_class.each(&block) if name == nil
|
123
|
+
return representers[name] if representers[name] # don't run block as this representer is already setup for this form class.
|
124
|
+
|
125
|
+
only_forms = options[:all] ? false : true
|
126
|
+
base = options[:superclass] || representer_class
|
127
|
+
|
128
|
+
representers[name] = Class.new(base).each(only_forms, &block) # let user modify representer.
|
129
|
+
end
|
130
|
+
|
101
131
|
require 'reform/contract/validate'
|
102
132
|
include Validate
|
103
133
|
|
104
|
-
|
105
134
|
def errors # FIXME: this is needed for Rails 3.0 compatibility.
|
106
135
|
@errors ||= Errors.new(self)
|
107
136
|
end
|
@@ -111,10 +140,35 @@ module Reform
|
|
111
140
|
attr_accessor :fields
|
112
141
|
attr_writer :errors # only used in top form. (is that true?)
|
113
142
|
|
114
|
-
def mapper
|
143
|
+
def mapper # FIXME: do we need this with class-level representers?
|
115
144
|
self.class.representer_class
|
116
145
|
end
|
117
146
|
|
147
|
+
def self.deprecate_as!(options) # TODO: remove me in 2.0.
|
148
|
+
return unless as = options.delete(:as)
|
149
|
+
options[:from] = as
|
150
|
+
warn "[Reform] The :as options got renamed to :from. See https://github.com/apotonick/reform/wiki/Migration-Guide and have a nice day."
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.deprecate_virtual_and_empty!(options) # TODO: remove me in 2.0.
|
154
|
+
if options.delete(:virtual)
|
155
|
+
warn "[Reform] The :virtual option has changed! Check https://github.com/apotonick/reform/wiki/Migration-Guide and have a good day."
|
156
|
+
options[:_readable] = true
|
157
|
+
options[:_writeable] = false
|
158
|
+
end
|
159
|
+
|
160
|
+
if options[:empty]
|
161
|
+
warn "[Reform] The :empty option has changed! Check https://github.com/apotonick/reform/wiki/Migration-Guide and have a good day."
|
162
|
+
options[:_readable] = false
|
163
|
+
options[:_writeable] = false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
inheritable_attr :reform_2_0 # TODO: remove me in 2.0.
|
168
|
+
def self.reform_2_0!
|
169
|
+
self.reform_2_0= true
|
170
|
+
end
|
171
|
+
|
118
172
|
def self.register_feature(mod)
|
119
173
|
features[mod] = true
|
120
174
|
end
|
@@ -6,14 +6,25 @@ module Reform
|
|
6
6
|
@fields = setup_fields # delegate all methods to Fields instance.
|
7
7
|
end
|
8
8
|
|
9
|
+
# Setup#to_hash will create a nested hash of property values from the model.
|
10
|
+
# Nested properties will be recursively wrapped in a form instance.
|
11
|
+
def setup_representer
|
12
|
+
self.class.representer(:setup) do |dfn| # only nested forms.
|
13
|
+
dfn.merge!(
|
14
|
+
:representable => false, # don't call #to_hash, only prepare.
|
15
|
+
:prepare => lambda { |model, args| args.binding[:form].new(model) } # wrap nested properties in form.
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
9
20
|
def setup_fields
|
10
|
-
representer =
|
21
|
+
representer = setup_representer.new(aliased_model)
|
11
22
|
options = setup_options(Reform::Representer::Options[]) # handles :empty.
|
12
23
|
|
24
|
+
# populate the internal @fields set with data from the model.
|
13
25
|
create_fields(mapper.fields, representer.to_hash(options))
|
14
26
|
end
|
15
27
|
|
16
|
-
# DISCUSS: setting up the Validation (populating with values) will soon be handled with Disposable::Twin logic.
|
17
28
|
def create_fields(field_names, fields)
|
18
29
|
Fields.new(field_names, fields)
|
19
30
|
end
|
@@ -26,31 +37,14 @@ module Reform
|
|
26
37
|
include SetupOptions
|
27
38
|
|
28
39
|
|
29
|
-
|
30
|
-
module Representer
|
31
|
-
def to_hash(*)
|
32
|
-
nested_forms do |attr|
|
33
|
-
attr.merge!(
|
34
|
-
:representable => false, # don't call #to_hash.
|
35
|
-
:prepare => lambda do |model, args|
|
36
|
-
args.binding[:form].new(model)
|
37
|
-
end
|
38
|
-
)
|
39
|
-
end
|
40
|
-
|
41
|
-
super
|
42
|
-
end
|
43
|
-
end # Representer
|
44
|
-
|
45
|
-
|
46
|
-
module Empty
|
40
|
+
module Readable
|
47
41
|
def setup_options(options)
|
48
|
-
empty_fields = mapper.representable_attrs.find_all { |d| d[:
|
42
|
+
empty_fields = mapper.representable_attrs.find_all { |d| d[:_readable] == false }.collect { |d| d.name.to_sym }
|
49
43
|
|
50
44
|
options.exclude!(empty_fields)
|
51
45
|
end
|
52
46
|
end
|
53
|
-
include
|
47
|
+
include Readable
|
54
48
|
end
|
55
49
|
end # Setup
|
56
50
|
end
|
@@ -1,24 +1,4 @@
|
|
1
1
|
module Reform::Contract::Validate
|
2
|
-
module NestedValid
|
3
|
-
def to_hash(*)
|
4
|
-
nested_forms do |attr|
|
5
|
-
attr.merge!(
|
6
|
-
:serialize => lambda { |object, args|
|
7
|
-
|
8
|
-
# FIXME: merge with Validate::Writer
|
9
|
-
options = args.user_options.dup
|
10
|
-
options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
|
11
|
-
options[:prefix] << args.binding.name # FIXME: should be #as.
|
12
|
-
|
13
|
-
object.validate!(options) # recursively call valid?
|
14
|
-
},
|
15
|
-
)
|
16
|
-
end
|
17
|
-
|
18
|
-
super
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
2
|
def validate
|
23
3
|
options = {:errors => errs = Reform::Contract::Errors.new(self), :prefix => []}
|
24
4
|
|
@@ -28,15 +8,32 @@ module Reform::Contract::Validate
|
|
28
8
|
|
29
9
|
errors.valid?
|
30
10
|
end
|
11
|
+
|
31
12
|
def validate!(options)
|
32
13
|
prefix = options[:prefix]
|
33
14
|
|
34
15
|
# call valid? recursively and collect nested errors.
|
35
|
-
|
16
|
+
valid_representer.new(fields).to_hash(options) # TODO: only include nested forms here.
|
36
17
|
|
37
18
|
valid? # this validates on <Fields> using AM::Validations, currently.
|
38
19
|
|
39
20
|
options[:errors].merge!(self.errors, prefix)
|
40
21
|
end
|
41
22
|
|
23
|
+
private
|
24
|
+
|
25
|
+
# runs form.validate! on all nested forms
|
26
|
+
def valid_representer
|
27
|
+
self.class.representer(:valid) do |dfn|
|
28
|
+
dfn.merge!(
|
29
|
+
:serialize => lambda { |form, args|
|
30
|
+
options = args.user_options.dup
|
31
|
+
options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
|
32
|
+
options[:prefix] << args.binding.name # FIXME: should be #as.
|
33
|
+
|
34
|
+
form.validate!(options) # recursively call valid? on nested form.
|
35
|
+
}
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
42
39
|
end
|
@@ -29,22 +29,20 @@ module Reform::Form::ActiveModel
|
|
29
29
|
return super unless params.is_a?(Hash)
|
30
30
|
# TODO: run this only for hash deserialization, but generically (#deserialize_hash ?).
|
31
31
|
|
32
|
-
|
33
|
-
rename_nested_param_for!(params, attr)
|
34
|
-
end
|
32
|
+
self.class.representer { |dfn| rename_nested_param_for!(params, dfn) }
|
35
33
|
|
36
34
|
super
|
37
35
|
end
|
38
36
|
|
39
37
|
private
|
40
|
-
def rename_nested_param_for!(params,
|
41
|
-
nested_name = "#{
|
38
|
+
def rename_nested_param_for!(params, dfn)
|
39
|
+
nested_name = "#{dfn.name}_attributes"
|
42
40
|
return unless params.has_key?(nested_name)
|
43
41
|
|
44
|
-
value = params["#{
|
45
|
-
value = value.values if
|
42
|
+
value = params["#{dfn.name}_attributes"]
|
43
|
+
value = value.values if dfn[:collection]
|
46
44
|
|
47
|
-
params[
|
45
|
+
params[dfn.name] = value
|
48
46
|
end
|
49
47
|
end # FormBuilderMethods
|
50
48
|
|