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
@@ -0,0 +1,27 @@
|
|
1
|
+
# Include this in every module that gets further included.
|
2
|
+
module Reform::Form::Module
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.extend Included
|
6
|
+
end
|
7
|
+
|
8
|
+
module Included # TODO: use representable's inheritance mechanism.
|
9
|
+
def included(base)
|
10
|
+
super
|
11
|
+
@instructions.each { |cfg| base.send(cfg[0], *cfg[1], &cfg[2]) } # property :name, {} do .. end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def property(*args, &block)
|
17
|
+
instructions << [:property, args, block]
|
18
|
+
end
|
19
|
+
def validates(*args, &block)
|
20
|
+
instructions << [:validates, args, block]
|
21
|
+
end
|
22
|
+
|
23
|
+
def instructions
|
24
|
+
@instructions ||= []
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Reform::Form.class_eval do
|
2
2
|
module MultiParameterAttributes
|
3
3
|
def self.included(base)
|
4
|
-
base.
|
4
|
+
base.send(:register_feature, self)
|
5
5
|
end
|
6
6
|
|
7
7
|
class DateParamsFilter
|
@@ -38,9 +38,29 @@ Reform::Form.class_eval do
|
|
38
38
|
def validate(params)
|
39
39
|
# TODO: make it cleaner to hook into essential reform steps.
|
40
40
|
# TODO: test with nested.
|
41
|
-
DateParamsFilter.new.call(params)
|
41
|
+
DateParamsFilter.new.call(params) if params.is_a?(Hash) # this currently works for hash, only.
|
42
42
|
|
43
43
|
super
|
44
44
|
end
|
45
|
+
|
46
|
+
|
47
|
+
# module ClassMethods
|
48
|
+
# def representer_class # TODO: check out how we can utilise Config#features.
|
49
|
+
# super.class_eval do
|
50
|
+
# extend BuildDefinition
|
51
|
+
# self
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
|
56
|
+
|
57
|
+
# module BuildDefinition
|
58
|
+
# def build_definition(name, options, &block)
|
59
|
+
# return super unless options[:multi_params]
|
60
|
+
|
61
|
+
# options[:parse_filter] << DateParamsFilter.new
|
62
|
+
# super
|
63
|
+
# end
|
64
|
+
# end
|
45
65
|
end
|
46
66
|
end
|
data/lib/reform/form/save.rb
CHANGED
@@ -14,9 +14,9 @@ module Reform::Form::Save
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
def save
|
17
|
+
def save(&block)
|
18
18
|
# DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
|
19
|
-
return
|
19
|
+
return deprecate_first_save_block_arg(&block) if block_given?
|
20
20
|
|
21
21
|
sync_models # recursion
|
22
22
|
save!
|
@@ -37,8 +37,7 @@ module Reform::Form::Save
|
|
37
37
|
# Transform form data into a nested hash for #save.
|
38
38
|
nested_forms do |attr|
|
39
39
|
attr.merge!(
|
40
|
-
:
|
41
|
-
:serialize => lambda { |object, args| object.to_nested_hash },
|
40
|
+
:serialize => lambda { |object, args| object.to_nested_hash }
|
42
41
|
)
|
43
42
|
end
|
44
43
|
|
@@ -50,10 +49,24 @@ module Reform::Form::Save
|
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
52
|
+
|
53
53
|
require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
|
54
|
-
def to_nested_hash
|
54
|
+
def to_nested_hash(*)
|
55
55
|
map = mapper.new(fields).extend(NestedHash)
|
56
56
|
|
57
57
|
ActiveSupport::HashWithIndifferentAccess.new(map.to_hash)
|
58
58
|
end
|
59
|
+
alias_method :to_hash, :to_nested_hash
|
60
|
+
# NOTE: it is not recommended using #to_hash and #to_nested_hash in your code, consider
|
61
|
+
# them private.
|
62
|
+
|
63
|
+
private
|
64
|
+
def deprecate_first_save_block_arg(&block)
|
65
|
+
if block.arity == 2
|
66
|
+
warn "[Reform] Deprecation Warning: The first block argument in `save { |form, hash| .. }` is deprecated and its new signature is `save { |hash| .. }`. If you need the form instance, use it in the block. Have a good day."
|
67
|
+
return yield(self, to_nested_hash)
|
68
|
+
end
|
69
|
+
|
70
|
+
yield to_nested_hash # new behaviour.
|
71
|
+
end
|
59
72
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Reform::Form::Scalar
|
2
|
+
# IDEA: what if every "leaf" property would be represented by a Scalar form?
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.extend Forwardable
|
6
|
+
end
|
7
|
+
|
8
|
+
def update!(object)
|
9
|
+
@fields = object # @scalar is "I came from the outside." or <ArbitraryObject>.
|
10
|
+
end
|
11
|
+
|
12
|
+
def scalar
|
13
|
+
fields
|
14
|
+
end
|
15
|
+
|
16
|
+
def save!
|
17
|
+
end
|
18
|
+
|
19
|
+
def sync!
|
20
|
+
model.replace(fields)
|
21
|
+
# FIXME: how to sync that, if it's not responds to replace? or what if we don't want to write (e.g. image with paperdragon)?
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_nested_hash
|
25
|
+
scalar
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
def validates(name, options={})
|
31
|
+
if name.is_a?(Hash)
|
32
|
+
name, options = :scalar, name # per default, validate #scalar (e.g. "Hello").
|
33
|
+
else
|
34
|
+
def_delegator :scalar, name
|
35
|
+
end
|
36
|
+
|
37
|
+
super(name, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# TODO: change the way i hook into ::property.
|
43
|
+
module Property
|
44
|
+
def property(name, options={}, &block)
|
45
|
+
if options[:scalar]
|
46
|
+
options.merge!(:features => [Reform::Form::Scalar], populate_if_empty: String)
|
47
|
+
end
|
48
|
+
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/reform/form/sync.rb
CHANGED
@@ -11,8 +11,8 @@ module Reform::Form::Sync
|
|
11
11
|
nested_forms do |attr|
|
12
12
|
attr.merge!(
|
13
13
|
:instance => lambda { |fragment, *| fragment },
|
14
|
-
:deserialize => lambda { |object, *| object.sync! }
|
15
|
-
:setter
|
14
|
+
:deserialize => lambda { |object, *| model = object.sync! } # sync! returns the synced model.
|
15
|
+
# representable's :setter will do collection=([..]) or property=(..) for us on the model.
|
16
16
|
)
|
17
17
|
end
|
18
18
|
|
@@ -34,7 +34,6 @@ module Reform::Form::Sync
|
|
34
34
|
:representable => false,
|
35
35
|
:prepare => lambda { |obj, *| obj }
|
36
36
|
)
|
37
|
-
|
38
37
|
end
|
39
38
|
|
40
39
|
super
|
@@ -42,8 +41,6 @@ module Reform::Form::Sync
|
|
42
41
|
end
|
43
42
|
|
44
43
|
|
45
|
-
### TODO: add ToHash with :prepare => lambda { |form, args| form },
|
46
|
-
|
47
44
|
def sync_models
|
48
45
|
sync!
|
49
46
|
end
|
@@ -56,6 +53,8 @@ module Reform::Form::Sync
|
|
56
53
|
|
57
54
|
input = input_representer.to_hash
|
58
55
|
|
59
|
-
mapper.new(aliased_model).extend(Writer).from_hash(input)
|
56
|
+
mapper.new(aliased_model).extend(Writer).from_hash(input) # sync properties to Song.
|
57
|
+
|
58
|
+
model
|
60
59
|
end
|
61
60
|
end
|
data/lib/reform/form/validate.rb
CHANGED
@@ -1,16 +1,29 @@
|
|
1
1
|
# Mechanics for writing to forms in #validate.
|
2
2
|
module Reform::Form::Validate
|
3
3
|
module Update
|
4
|
+
# IDEA: what if Populate was a Decorator that simply knows how to setup the Form object graph, nothing more? That would decouple
|
5
|
+
# the population from the validation (good and bad as less customizable).
|
6
|
+
|
7
|
+
# Go through all nested forms and call form.update!(hash).
|
4
8
|
def from_hash(*)
|
5
9
|
nested_forms do |attr|
|
6
|
-
attr.delete(:prepare)
|
7
|
-
attr.delete(:extend)
|
8
|
-
|
9
10
|
attr.merge!(
|
11
|
+
# set parse_strategy: sync> # DISCUSS: that kills the :setter directive, which usually sucks. at least document this in :populator.
|
10
12
|
:collection => attr[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in attr YET.
|
11
13
|
:parse_strategy => :sync, # just use nested objects as they are.
|
14
|
+
|
12
15
|
:deserialize => lambda { |object, params, args| object.update!(params) },
|
13
16
|
)
|
17
|
+
|
18
|
+
# TODO: :populator now is just an alias for :instance. handle in ::property.
|
19
|
+
attr.merge!(:instance => attr[:populator]) if attr[:populator]
|
20
|
+
|
21
|
+
attr.merge!(:instance => Populator::PopulateIfEmpty.new) if attr[:populate_if_empty]
|
22
|
+
end
|
23
|
+
|
24
|
+
# FIXME: solve this with a dedicated Populate Decorator per Form.
|
25
|
+
representable_attrs.each do |attr|
|
26
|
+
attr.merge!(:parse_filter => Representable::Coercion::Coercer.new(attr[:coercion_type])) if attr[:coercion_type]
|
14
27
|
end
|
15
28
|
|
16
29
|
super
|
@@ -19,26 +32,26 @@ module Reform::Form::Validate
|
|
19
32
|
|
20
33
|
|
21
34
|
module Populator
|
35
|
+
# This might change soon (e.g. moved into disposable).
|
22
36
|
class PopulateIfEmpty
|
23
|
-
|
24
|
-
@fields, @fragment, args = args
|
25
|
-
@index = args.first
|
26
|
-
@args = args.last
|
27
|
-
end
|
37
|
+
include Uber::Callable
|
28
38
|
|
29
|
-
def call
|
30
|
-
|
39
|
+
def call(fields, fragment, *args)
|
40
|
+
index = args.first
|
41
|
+
options = args.last
|
42
|
+
binding = options.binding
|
31
43
|
form = binding.get
|
32
44
|
|
33
|
-
parent_form =
|
34
|
-
form_model = parent_form.model # FIXME: sort out who's responsible for sync.
|
45
|
+
parent_form = options.user_options[:parent_form]
|
35
46
|
|
36
|
-
|
37
|
-
return if
|
47
|
+
# FIXME: test those cases!!!
|
48
|
+
return form[index] if binding.array? and form and form[index] # TODO: this should be handled by the Binding.
|
49
|
+
return form if !binding.array? and form
|
38
50
|
# only get here when above form is nil.
|
39
51
|
|
52
|
+
|
40
53
|
if binding[:populate_if_empty].is_a?(Proc)
|
41
|
-
model = parent_form.instance_exec(
|
54
|
+
model = parent_form.instance_exec(fragment, options.user_options, &binding[:populate_if_empty]) # call user block.
|
42
55
|
else
|
43
56
|
model = binding[:populate_if_empty].new
|
44
57
|
end
|
@@ -46,65 +59,35 @@ module Reform::Form::Validate
|
|
46
59
|
form = binding[:form].new(model) # free service: wrap model with Form. this usually happens in #setup.
|
47
60
|
|
48
61
|
if binding.array?
|
49
|
-
|
50
|
-
@fields.send("#{binding.getter}")[@index] = form
|
62
|
+
fields.send("#{binding.getter}")[index] = form
|
51
63
|
else
|
52
|
-
|
53
|
-
@fields.send("#{binding.setter}", form) # :setter is currently overwritten by :parse_strategy.
|
64
|
+
fields.send("#{binding.setter}", form) # :setter is currently overwritten by :parse_strategy.
|
54
65
|
end
|
55
66
|
end
|
56
67
|
end # PopulateIfEmpty
|
57
|
-
|
58
|
-
|
59
|
-
def from_hash(params, args)
|
60
|
-
populated_attrs = []
|
61
|
-
|
62
|
-
nested_forms do |attr|
|
63
|
-
next unless attr[:populate_if_empty]
|
64
|
-
|
65
|
-
attr.merge!(
|
66
|
-
# DISCUSS: it would be cool to move the lambda block to PopulateIfEmpty#call.
|
67
|
-
:populator => lambda do |fragment, *args|
|
68
|
-
PopulateIfEmpty.new(self, fragment, args).call
|
69
|
-
end
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
nested_forms do |attr|
|
75
|
-
next unless attr[:populator]
|
76
|
-
|
77
|
-
attr.merge!(
|
78
|
-
:parse_strategy => attr[:populator],
|
79
|
-
:representable => false
|
80
|
-
)
|
81
|
-
populated_attrs << attr.name.to_sym
|
82
|
-
end
|
83
|
-
|
84
|
-
super(params, {:include => populated_attrs}.merge(args))
|
85
|
-
end
|
86
68
|
end
|
87
69
|
|
88
|
-
|
70
|
+
# 1. Populate the form object graph so that each incoming object has a representative form object.
|
71
|
+
# 2. Deserialize. This is wrong and should be done in 1.
|
72
|
+
# 3. Validate the form object graph.
|
89
73
|
def validate(params)
|
90
74
|
update!(params)
|
91
75
|
|
92
|
-
super()
|
76
|
+
super() # run the actual validation on self.
|
93
77
|
end
|
94
78
|
|
95
79
|
def update!(params)
|
96
|
-
populate!(params)
|
97
80
|
deserialize!(params)
|
98
81
|
end
|
99
82
|
|
100
83
|
private
|
101
|
-
def populate!(params)
|
102
|
-
# populate only happens for nested forms, if you override that setter it's your fault.
|
103
|
-
mapper.new(fields).extend(Populator).from_hash(params, :parent_form => self) # TODO: remove model(form) once we found out how to synchronize the model correctly. see https://github.com/apotonick/reform/issues/86#issuecomment-43402047
|
104
|
-
end
|
105
|
-
|
106
84
|
def deserialize!(params)
|
107
85
|
# using self here will call the form's setters like title= which might be overridden.
|
108
|
-
|
86
|
+
# from_hash(params, parent_form: self)
|
87
|
+
mapper.new(self).extend(Update).send(deserialize_method, params, :parent_form => self)
|
88
|
+
end
|
89
|
+
|
90
|
+
def deserialize_method
|
91
|
+
:from_hash
|
109
92
|
end
|
110
93
|
end
|
data/lib/reform/rails.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'reform/form/active_model'
|
2
|
-
|
3
|
-
|
4
|
-
end
|
2
|
+
|
3
|
+
require 'reform/form/active_record' if defined?(ActiveRecord)
|
5
4
|
|
6
5
|
Reform::Form.class_eval do # DISCUSS: i'd prefer having a separate Rails module to be mixed into the Form but this is way more convenient for 99% users.
|
7
6
|
include Reform::Form::ActiveModel
|
8
7
|
include Reform::Form::ActiveModel::FormBuilderMethods
|
8
|
+
include Reform::Form::ActiveRecord if defined?(ActiveRecord)
|
9
9
|
end
|
data/lib/reform/representer.rb
CHANGED
@@ -6,9 +6,10 @@ module Reform
|
|
6
6
|
include Representable::Hash::AllowSymbols
|
7
7
|
|
8
8
|
extend Uber::InheritableAttr
|
9
|
-
inheritable_attr :options
|
9
|
+
inheritable_attr :options # FIXME: this doesn't need to be inheritable.
|
10
10
|
# self.options = {}
|
11
11
|
|
12
|
+
|
12
13
|
# Invokes #to_hash and/or #from_hash with #options. This provides a hook for other
|
13
14
|
# modules to add options for the representational process.
|
14
15
|
module WithOptions
|
@@ -58,6 +59,10 @@ module Reform
|
|
58
59
|
end
|
59
60
|
end
|
60
61
|
|
62
|
+
def self.default_inline_class
|
63
|
+
options[:form_class]
|
64
|
+
end
|
65
|
+
|
61
66
|
def self.clone # called in inheritable_attr :representer_class.
|
62
67
|
Class.new(self) # By subclassing, representable_attrs.clone is called.
|
63
68
|
end
|
@@ -66,18 +71,20 @@ module Reform
|
|
66
71
|
def clone_config!
|
67
72
|
# TODO: representable_attrs.clone! which does exactly what's done below.
|
68
73
|
attrs = Representable::Config.new
|
69
|
-
attrs.inherit(representable_attrs) # since in every use case we modify Config we clone.
|
74
|
+
attrs.inherit!(representable_attrs) # since in every use case we modify Config we clone.
|
70
75
|
@representable_attrs = attrs
|
71
76
|
end
|
72
77
|
|
73
|
-
|
78
|
+
# Inline forms always get saved in :extend.
|
79
|
+
def self.build_inline(base, features, name, options, &block)
|
74
80
|
name = name.to_s.singularize.camelize
|
75
81
|
|
76
|
-
|
77
|
-
|
78
|
-
|
82
|
+
features = options[:features]
|
83
|
+
|
84
|
+
Class.new(base || default_inline_class) do
|
85
|
+
include *features
|
79
86
|
|
80
|
-
|
87
|
+
class_eval &block
|
81
88
|
|
82
89
|
@form_name = name
|
83
90
|
|
data/lib/reform/version.rb
CHANGED
data/reform.gemspec
CHANGED
@@ -18,15 +18,17 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "representable", "~>
|
22
|
-
spec.add_dependency "disposable", "~> 0.0.
|
23
|
-
spec.add_dependency "uber", "~> 0.0.
|
21
|
+
spec.add_dependency "representable", "~> 2.0.3"
|
22
|
+
spec.add_dependency "disposable", "~> 0.0.5"
|
23
|
+
spec.add_dependency "uber", "~> 0.0.8"
|
24
24
|
spec.add_dependency "activemodel"
|
25
25
|
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
-
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rake"
|
27
27
|
spec.add_development_dependency "minitest", "4.2.0"
|
28
28
|
spec.add_development_dependency "activerecord"
|
29
29
|
spec.add_development_dependency "sqlite3"
|
30
30
|
spec.add_development_dependency "virtus"
|
31
31
|
spec.add_development_dependency "rails"
|
32
|
+
|
33
|
+
spec.add_development_dependency "actionpack"
|
32
34
|
end
|
data/test/active_model_test.rb
CHANGED
@@ -110,10 +110,9 @@ class ActiveModelWithCompositionTest < MiniTest::Spec
|
|
110
110
|
let (:duran) { OpenStruct.new }
|
111
111
|
let (:form) { HitForm.new(:song => rio, :artist => duran) }
|
112
112
|
|
113
|
-
describe "
|
114
|
-
it
|
115
|
-
|
116
|
-
end
|
113
|
+
describe "model accessors a la model#[:hit]" do
|
114
|
+
it { form.model[:song].must_equal rio }
|
115
|
+
it { form.model[:artist].must_equal duran }
|
117
116
|
|
118
117
|
it "doesn't delegate when :on missing" do
|
119
118
|
class SongOnlyForm < Reform::Form
|
@@ -123,29 +122,11 @@ class ActiveModelWithCompositionTest < MiniTest::Spec
|
|
123
122
|
property :title, :on => :song
|
124
123
|
|
125
124
|
model :song
|
126
|
-
end.new(:song => rio, :artist => duran).song.must_equal rio
|
125
|
+
end.new(:song => rio, :artist => duran).model[:song].must_equal rio
|
127
126
|
end
|
128
|
-
|
129
|
-
# it "delegates when you call ::model" do
|
130
|
-
# class SongOnlyForm < Reform::Form
|
131
|
-
# include Composition
|
132
|
-
# include Reform::Form::ActiveModel
|
133
|
-
|
134
|
-
# property :title, :on => :song
|
135
|
-
# model :song
|
136
|
-
|
137
|
-
# self
|
138
|
-
# end.new(:song => rio, :artist => duran).persisted?
|
139
|
-
# end
|
140
127
|
end
|
141
128
|
|
142
129
|
|
143
|
-
it "creates composition readers" do
|
144
|
-
skip "we don't want those anymore since they don't represent the form internal state!"
|
145
|
-
form.song.must_equal rio
|
146
|
-
form.artist.must_equal duran
|
147
|
-
end
|
148
|
-
|
149
130
|
it "provides ::model_name" do
|
150
131
|
form.class.model_name.must_equal "Hit"
|
151
132
|
end
|