reform 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|