reform 1.0.4 → 1.1.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 +4 -4
- data/CHANGES.md +13 -0
- data/Gemfile +1 -1
- data/README.md +61 -13
- data/database.sqlite3 +0 -0
- data/lib/reform/composition.rb +1 -0
- data/lib/reform/contract.rb +24 -15
- data/lib/reform/form.rb +5 -0
- data/lib/reform/form/active_model.rb +5 -3
- data/lib/reform/form/active_model/model_validations.rb +98 -0
- data/lib/reform/form/active_record.rb +8 -1
- data/lib/reform/form/coercion.rb +4 -4
- data/lib/reform/form/composition.rb +2 -19
- data/lib/reform/form/module.rb +27 -0
- data/lib/reform/form/multi_parameter_attributes.rb +22 -2
- data/lib/reform/form/save.rb +18 -5
- data/lib/reform/form/scalar.rb +52 -0
- data/lib/reform/form/sync.rb +5 -6
- data/lib/reform/form/validate.rb +40 -57
- data/lib/reform/rails.rb +3 -3
- data/lib/reform/representer.rb +14 -7
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +6 -4
- data/test/active_model_test.rb +4 -23
- data/test/active_record_test.rb +188 -42
- data/test/as_test.rb +1 -1
- data/test/builder_test.rb +32 -0
- data/test/coercion_test.rb +56 -28
- data/test/deserialize_test.rb +40 -0
- data/test/form_builder_test.rb +1 -1
- data/test/form_composition_test.rb +50 -22
- data/test/inherit_test.rb +79 -0
- data/test/model_validations_test.rb +82 -0
- data/test/nested_form_test.rb +2 -13
- data/test/reform_test.rb +106 -7
- data/test/scalar_test.rb +167 -0
- data/test/test_helper.rb +10 -0
- data/test/validate_test.rb +42 -2
- metadata +37 -12
- data/test/setup_test.rb +0 -68
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b95fac93b5186e174e6e2aef263bff4bf5305210
|
4
|
+
data.tar.gz: bafdc2c15f78fa1fe27e9b554c32d3aafcfb4fbd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a55abac9d331f20d75fa66ed8950c35e5cd16460fc054a182c6d0cfb0c4d9e1524349553b9aeeeafa33b75453d35ce3c8476ad8f0adce7775c41d99d4176338a
|
7
|
+
data.tar.gz: 61d436b3f4cb696e2cffa02f7e3ff1179741209da8a4f2073e36da45d12971983ff19be9ffdcb0b515ed601f933240685287fa208287a63f521d3d4dbcb2e99c
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## 1.1.0
|
2
|
+
|
3
|
+
* Deprecate first block argument in save. It's new signature is `save { |hash| }`. You already got the form instance when calling `form.save` so there's no need to pass it into the block.
|
4
|
+
* `#validate` does **not** touch any model anymore. Both single values and collections are written to the model after `#sync` or `#save`.
|
5
|
+
* Coercion now happens in `#validate`, only.
|
6
|
+
* You can now define forms in modules including `Reform::Form::Module` to improve reusability.
|
7
|
+
* Inheriting from forms and then overriding/extending properties with `:inherit` now works properly.
|
8
|
+
* You can now define methods in inline forms.
|
9
|
+
* Forms can now also deserialize other formats, e.g. JSON. This allows them to be used as a contract for API endpoints and in Operations in Trailblazer.
|
10
|
+
* Composition forms no longer expose readers to the composition members. The composition is available via `Form#model`, members via `Form#model[:member_name]`.
|
11
|
+
* ActiveRecord support is now included correctly and passed on to nested forms.
|
12
|
+
* Undocumented/Experimental: Scalar forms. This is still WIP.
|
13
|
+
|
1
14
|
## 1.0.4
|
2
15
|
|
3
16
|
Reverting what I did in 1.0.3. Leave your code as it is. You may override a writers like `#title=` to sanitize or filter incoming data, as in
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -147,24 +147,29 @@ Sometimes, you need to do stuff manually.
|
|
147
147
|
|
148
148
|
## Saving Forms Manually
|
149
149
|
|
150
|
-
|
150
|
+
Calling `#save` with a block doesn't do anything but providing you a nested hash with all the validated input. This allows you to implement the saving yourself.
|
151
151
|
|
152
|
-
|
152
|
+
The block parameter is a nested hash of the form input.
|
153
153
|
|
154
154
|
```ruby
|
155
|
-
|
155
|
+
@form.save do |hash|
|
156
|
+
hash #=> {title: "Rio", length: "366"}
|
156
157
|
|
157
|
-
|
158
|
-
|
159
|
-
|
158
|
+
Song.create(hash)
|
159
|
+
end
|
160
|
+
```
|
160
161
|
|
161
|
-
|
162
|
+
You can always access the form's model. This is helpful when you were using populators to set up objects when validating.
|
162
163
|
|
163
|
-
|
164
|
-
|
164
|
+
```ruby
|
165
|
+
@form.save do |nested|
|
166
|
+
album = @form.album.model
|
167
|
+
|
168
|
+
album.update_attributes(nested[:album])
|
169
|
+
end
|
165
170
|
```
|
166
171
|
|
167
|
-
|
172
|
+
Note that you can call `#sync` and _then_ call `#save { |hsh| }` to save models yourself.
|
168
173
|
|
169
174
|
|
170
175
|
## Contracts
|
@@ -200,12 +205,14 @@ In future versions and with the upcoming [Trailblazer framework](https://github.
|
|
200
205
|
Applying a contract is simple, all you need is a populated object (e.g. an album after `#update_attributes`).
|
201
206
|
|
202
207
|
```ruby
|
203
|
-
album.
|
208
|
+
album.assign_attributes(..)
|
204
209
|
|
205
|
-
|
210
|
+
contract = AlbumContract.new(album)
|
211
|
+
|
212
|
+
if contract.validate
|
206
213
|
album.save
|
207
214
|
else
|
208
|
-
raise
|
215
|
+
raise contract.errors.messages.inspect
|
209
216
|
end
|
210
217
|
```
|
211
218
|
|
@@ -486,6 +493,47 @@ Here's how the block parameters look like.
|
|
486
493
|
end
|
487
494
|
```
|
488
495
|
|
496
|
+
## Inheritance
|
497
|
+
|
498
|
+
Forms can be derived from other forms and will inherit all properties and validations.
|
499
|
+
|
500
|
+
```ruby
|
501
|
+
class AlbumForm < Reform::Form
|
502
|
+
property :title
|
503
|
+
|
504
|
+
collection :songs do
|
505
|
+
property :title
|
506
|
+
|
507
|
+
validates :title, presence: true
|
508
|
+
end
|
509
|
+
end
|
510
|
+
```
|
511
|
+
|
512
|
+
Now, a simple inheritance can add fields.
|
513
|
+
|
514
|
+
```ruby
|
515
|
+
class CompilationForm < AlbumForm
|
516
|
+
property :composers do
|
517
|
+
property :name
|
518
|
+
end
|
519
|
+
end
|
520
|
+
```
|
521
|
+
|
522
|
+
This will _add_ `composers` to the existing fields.
|
523
|
+
|
524
|
+
You can also partially override fields using `:inherit`.
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
class CompilationForm < AlbumForm
|
528
|
+
property :songs, inherit: true do
|
529
|
+
property :band_id
|
530
|
+
validates :band_id, presence: true
|
531
|
+
end
|
532
|
+
end
|
533
|
+
```
|
534
|
+
|
535
|
+
Using `inherit:` here will extend the existing `songs` form with the `band_id` field. Note that this simply uses [representable's inheritance mechanism](https://github.com/apotonick/representable/#partly-overriding-properties).
|
536
|
+
|
489
537
|
## Coercion
|
490
538
|
|
491
539
|
Often you want incoming form data to be converted to a type, like timestamps. Reform uses [virtus](https://github.com/solnic/virtus) for coercion, the DSL is seamlessly integrated into Reform with the `:type` option.
|
data/database.sqlite3
CHANGED
Binary file
|
data/lib/reform/composition.rb
CHANGED
data/lib/reform/contract.rb
CHANGED
@@ -1,31 +1,40 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'uber/inheritable_attr'
|
3
|
+
require 'uber/delegates'
|
3
4
|
|
4
5
|
require 'reform/representer'
|
5
6
|
|
6
7
|
module Reform
|
7
8
|
# Gives you a DSL for defining the object structure and its validations.
|
8
9
|
class Contract # DISCUSS: make class?
|
9
|
-
extend
|
10
|
+
extend Uber::Delegates
|
10
11
|
|
11
12
|
extend Uber::InheritableAttr
|
13
|
+
# representer_class gets inherited (cloned) to subclasses.
|
12
14
|
inheritable_attr :representer_class
|
13
|
-
self.representer_class = Reform::Representer.for(:form_class => self)
|
15
|
+
self.representer_class = Reform::Representer.for(:form_class => self) # only happens in Contract/Form.
|
16
|
+
# this should be the only mechanism to inherit, features should be stored in this as well.
|
14
17
|
|
18
|
+
|
19
|
+
# each contract keeps track of its features and passes them onto its local representer_class.
|
20
|
+
# gets inherited, features get automatically included into inline representer.
|
21
|
+
# TODO: the representer class should handle that, e.g. in options (deep-clone when inheriting.)
|
15
22
|
inheritable_attr :features
|
16
|
-
self.features =
|
23
|
+
self.features = {}
|
24
|
+
|
17
25
|
|
18
26
|
RESERVED_METHODS = [:model, :aliased_model, :fields, :mapper] # TODO: refactor that so we don't need that.
|
19
27
|
|
20
28
|
|
21
29
|
module PropertyMethods
|
22
|
-
extend Forwardable
|
23
|
-
|
24
30
|
def property(name, options={}, &block)
|
25
31
|
options[:private_name] = options.delete(:as)
|
26
32
|
|
27
|
-
|
28
|
-
|
33
|
+
options[:coercion_type] = options.delete(:type)
|
34
|
+
|
35
|
+
options[:features] ||= []
|
36
|
+
options[:features] += features.keys if block_given?
|
37
|
+
|
29
38
|
definition = representer_class.property(name, options, &block)
|
30
39
|
setup_form_definition(definition) if block_given? or options[:form]
|
31
40
|
|
@@ -45,7 +54,8 @@ module Reform
|
|
45
54
|
|
46
55
|
def setup_form_definition(definition)
|
47
56
|
options = {
|
48
|
-
|
57
|
+
# TODO: make this a bit nicer. why do we need :form at all?
|
58
|
+
:form => (definition[:extend] and definition[:extend].evaluate(nil)) || definition[:form], # :form is always just a Form class name.
|
49
59
|
:pass_options => true, # new style of passing args
|
50
60
|
:prepare => lambda { |form, args| form }, # always just return the form without decorating.
|
51
61
|
:representable => true, # form: Class must be treated as a typed property.
|
@@ -58,13 +68,7 @@ module Reform
|
|
58
68
|
def create_accessor(name)
|
59
69
|
handle_reserved_names(name)
|
60
70
|
|
61
|
-
|
62
|
-
# so they can be overridden but still are callable with super.
|
63
|
-
accessors = Module.new do
|
64
|
-
extend Forwardable # DISCUSS: do we really need Forwardable here?
|
65
|
-
delegate [name, "#{name}="] => :fields
|
66
|
-
end
|
67
|
-
include accessors
|
71
|
+
delegates :fields, name, "#{name}=" # Uber::Delegates
|
68
72
|
end
|
69
73
|
|
70
74
|
def handle_reserved_names(name)
|
@@ -79,6 +83,7 @@ module Reform
|
|
79
83
|
include ActiveModel::Validations
|
80
84
|
|
81
85
|
|
86
|
+
|
82
87
|
attr_accessor :model
|
83
88
|
|
84
89
|
require 'reform/contract/setup'
|
@@ -100,6 +105,10 @@ module Reform
|
|
100
105
|
self.class.representer_class
|
101
106
|
end
|
102
107
|
|
108
|
+
def self.register_feature(mod)
|
109
|
+
features[mod] = true
|
110
|
+
end
|
111
|
+
|
103
112
|
alias_method :aliased_model, :model
|
104
113
|
|
105
114
|
|
data/lib/reform/form.rb
CHANGED
@@ -2,6 +2,7 @@ require 'ostruct'
|
|
2
2
|
|
3
3
|
require 'reform/contract'
|
4
4
|
require 'reform/composition'
|
5
|
+
require 'reform/form/module'
|
5
6
|
|
6
7
|
module Reform
|
7
8
|
class Form < Contract
|
@@ -24,5 +25,9 @@ module Reform
|
|
24
25
|
# TODO: cache the Expose.from class!
|
25
26
|
Reform::Expose.from(mapper).new(:model => model)
|
26
27
|
end
|
28
|
+
|
29
|
+
|
30
|
+
require 'reform/form/scalar'
|
31
|
+
extend Scalar::Property # experimental feature!
|
27
32
|
end
|
28
33
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
require 'reform/form/active_model/model_validations'
|
2
|
+
|
1
3
|
module Reform::Form::ActiveModel
|
2
4
|
module FormBuilderMethods # TODO: rename to FormBuilderCompat.
|
3
5
|
def self.included(base)
|
4
6
|
base.class_eval do
|
5
7
|
extend ClassMethods # ::model_name
|
6
|
-
|
8
|
+
register_feature FormBuilderMethods
|
7
9
|
end
|
8
10
|
end
|
9
11
|
|
@@ -48,9 +50,9 @@ module Reform::Form::ActiveModel
|
|
48
50
|
def self.included(base)
|
49
51
|
base.class_eval do
|
50
52
|
extend ClassMethods
|
51
|
-
|
53
|
+
register_feature ActiveModel
|
52
54
|
|
53
|
-
|
55
|
+
delegates :model, *[:persisted?, :to_key, :to_param, :id] # Uber::Delegates
|
54
56
|
|
55
57
|
def to_model # this is called somewhere in FormBuilder and ActionController.
|
56
58
|
self
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Reform::Form::ActiveModel
|
2
|
+
module ModelValidations
|
3
|
+
# TODO: extract Composition behaviour.
|
4
|
+
# reduce code in Mapping.
|
5
|
+
|
6
|
+
class ValidationCopier
|
7
|
+
|
8
|
+
def self.copy(form_class, mapping, models)
|
9
|
+
if models.is_a?(Hash)
|
10
|
+
models.each do |model_name, model|
|
11
|
+
new(form_class, mapping, model, model_name).copy
|
12
|
+
end
|
13
|
+
else
|
14
|
+
new(form_class, mapping, models).copy
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(form_class, mapping, model, model_name=nil)
|
19
|
+
@form_class = form_class
|
20
|
+
@mapping = mapping
|
21
|
+
@model = model
|
22
|
+
@model_name = model_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def copy
|
26
|
+
@model.validators.each(&method(:add_validator))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def add_validator(validator)
|
32
|
+
attributes = inverse_map_attributes(validator.attributes)
|
33
|
+
if attributes.any?
|
34
|
+
@form_class.validates(*attributes, {validator.kind => validator.options})
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def inverse_map_attributes(attributes)
|
39
|
+
@mapping.inverse_image(create_attributes(attributes))
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_attributes(attributes)
|
43
|
+
attributes.map do |attribute|
|
44
|
+
[@model_name, attribute].compact
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
class Mapping
|
51
|
+
def self.from_representable_attrs(attrs)
|
52
|
+
new.tap do |mapping|
|
53
|
+
attrs.each do |dfn|
|
54
|
+
from = dfn.name.to_sym
|
55
|
+
to = [dfn[:on], (dfn[:private_name] || dfn.name)].compact.map(&:to_sym)
|
56
|
+
mapping.add(from, to)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize
|
62
|
+
@forward_map = {}
|
63
|
+
@inverse_map = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# from is a symbol attribute
|
67
|
+
# to is an 1 or 2 element array, depending on whether the attribute is 'namespaced', as it is with composite forms.
|
68
|
+
# eg, add(:phone_number, [:person, :phone])
|
69
|
+
def add(from, to)
|
70
|
+
raise 'Mapping is not one-to-one' if @forward_map.has_key?(from) || @inverse_map.has_key?(to)
|
71
|
+
@forward_map[from] = to
|
72
|
+
@inverse_map[to] = from
|
73
|
+
end
|
74
|
+
|
75
|
+
def forward_image(attrs)
|
76
|
+
@forward_map.values_at(*attrs).compact
|
77
|
+
end
|
78
|
+
|
79
|
+
def forward(attr)
|
80
|
+
@forward_map[attr]
|
81
|
+
end
|
82
|
+
|
83
|
+
def inverse_image(attrs)
|
84
|
+
@inverse_map.values_at(*attrs).compact
|
85
|
+
end
|
86
|
+
|
87
|
+
def inverse(attr)
|
88
|
+
@inverse_map[attr]
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def copy_validations_from(models)
|
94
|
+
ValidationCopier.copy(self, Mapping.from_representable_attrs(representer_class.representable_attrs), models)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Reform::Form::ActiveRecord
|
2
2
|
def self.included(base)
|
3
3
|
base.class_eval do
|
4
|
+
register_feature Reform::Form::ActiveRecord
|
4
5
|
include Reform::Form::ActiveModel
|
5
6
|
extend ClassMethods
|
6
7
|
end
|
@@ -37,7 +38,13 @@ module Reform::Form::ActiveRecord
|
|
37
38
|
def model_for_property(name)
|
38
39
|
return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
|
39
40
|
|
40
|
-
model_name = mapper.representable_attrs
|
41
|
+
model_name = mapper.representable_attrs.get(name)[:on]
|
41
42
|
model[model_name]
|
42
43
|
end
|
44
|
+
|
45
|
+
# Delegate column for attribute to the model to support simple_form's
|
46
|
+
# attribute type interrogation.
|
47
|
+
def column_for_attribute(name)
|
48
|
+
model_for_property(name).column_for_attribute(name)
|
49
|
+
end
|
43
50
|
end
|
data/lib/reform/form/coercion.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
-
require 'representable/
|
1
|
+
require 'representable/coercion'
|
2
2
|
|
3
3
|
module Reform::Form::Coercion
|
4
4
|
def self.included(base)
|
5
5
|
base.extend(ClassMethods)
|
6
|
-
base.
|
6
|
+
base.send(:register_feature, self)
|
7
7
|
end
|
8
8
|
|
9
9
|
module ClassMethods
|
10
|
-
def representer_class
|
10
|
+
def representer_class # TODO: check out how we can utilise Config#features.
|
11
11
|
super.class_eval do
|
12
|
-
include Representable::
|
12
|
+
include Representable::Coercion
|
13
13
|
self
|
14
14
|
end
|
15
15
|
end
|
@@ -16,12 +16,6 @@ module Reform::Form::Composition
|
|
16
16
|
@model_class ||= Reform::Composition.from(representer_class)
|
17
17
|
end
|
18
18
|
|
19
|
-
def property(name, options={})
|
20
|
-
super.tap do |definition|
|
21
|
-
handle_deprecated_model_accessor(options[:on]) unless options[:skip_accessors] # TODO: remove in 1.2.
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
19
|
# Same as ActiveModel::model but allows you to define the main model in the composition
|
26
20
|
# using +:on+.
|
27
21
|
#
|
@@ -32,8 +26,6 @@ module Reform::Form::Composition
|
|
32
26
|
|
33
27
|
composition_model = options[:on] || main_model
|
34
28
|
|
35
|
-
handle_deprecated_model_accessor(composition_model) unless options[:skip_accessors] # TODO: remove in 1.2.
|
36
|
-
|
37
29
|
# FIXME: this should just delegate to :model as in FB, and the comp would take care of it internally.
|
38
30
|
[:persisted?, :to_key, :to_param].each do |method|
|
39
31
|
define_method method do
|
@@ -41,16 +33,7 @@ module Reform::Form::Composition
|
|
41
33
|
end
|
42
34
|
end
|
43
35
|
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
def handle_deprecated_model_accessor(name, aliased=name)
|
49
|
-
define_method name do # form.band -> composition.band
|
50
|
-
warn %{[Reform] Deprecation WARNING: When using Composition, you may not call Form##{name} anymore to access the contained model. Please use Form#model[:#{name}] and have a lovely day!}
|
51
|
-
|
52
|
-
@model[name]
|
53
|
-
end
|
36
|
+
self
|
54
37
|
end
|
55
38
|
end
|
56
39
|
|
@@ -64,7 +47,7 @@ module Reform::Form::Composition
|
|
64
47
|
end
|
65
48
|
|
66
49
|
def to_hash(*args)
|
67
|
-
mapper.new(fields).to_hash(*args)
|
50
|
+
mapper.new(fields).to_hash(*args) # do not map names, yet. this happens in #to_nested_hash
|
68
51
|
end
|
69
52
|
|
70
53
|
private
|