reform 1.1.1 → 1.2.0.beta1
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 +35 -1
- data/Gemfile +1 -1
- data/README.md +83 -21
- data/TODO.md +8 -0
- data/database.sqlite3 +0 -0
- data/gemfiles/Gemfile.rails-4.0 +1 -0
- data/lib/reform.rb +4 -2
- data/lib/reform/active_record.rb +2 -1
- data/lib/reform/composition.rb +2 -2
- data/lib/reform/contract.rb +24 -7
- data/lib/reform/contract/setup.rb +21 -9
- data/lib/reform/contract/validate.rb +0 -6
- data/lib/reform/form.rb +6 -8
- data/lib/reform/form/active_model.rb +3 -2
- data/lib/reform/form/active_model/model_validations.rb +13 -1
- data/lib/reform/form/active_record.rb +1 -7
- data/lib/reform/form/changed.rb +9 -0
- data/lib/reform/form/json.rb +13 -0
- data/lib/reform/form/model_reflections.rb +18 -0
- data/lib/reform/form/save.rb +25 -3
- data/lib/reform/form/scalar.rb +4 -2
- data/lib/reform/form/sync.rb +82 -12
- data/lib/reform/form/validate.rb +38 -0
- data/lib/reform/rails.rb +1 -1
- data/lib/reform/representer.rb +14 -23
- data/lib/reform/schema.rb +23 -0
- data/lib/reform/twin.rb +20 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +2 -2
- data/test/active_model_test.rb +2 -2
- data/test/active_record_test.rb +7 -4
- data/test/changed_test.rb +69 -0
- data/test/custom_validation_test.rb +47 -0
- data/test/deserialize_test.rb +2 -7
- data/test/empty_test.rb +30 -0
- data/test/fields_test.rb +24 -0
- data/test/form_composition_test.rb +24 -2
- data/test/form_test.rb +84 -0
- data/test/inherit_test.rb +12 -0
- data/test/model_reflections_test.rb +65 -0
- data/test/read_only_test.rb +28 -0
- data/test/reform_test.rb +2 -175
- data/test/representer_test.rb +47 -0
- data/test/save_test.rb +51 -1
- data/test/scalar_test.rb +0 -18
- data/test/skip_if_test.rb +62 -0
- data/test/skip_unchanged_test.rb +86 -0
- data/test/sync_option_test.rb +83 -0
- data/test/twin_test.rb +23 -0
- data/test/validate_test.rb +9 -1
- metadata +37 -9
- data/lib/reform/form/virtual_attributes.rb +0 -22
@@ -2,9 +2,6 @@ module Reform::Contract::Validate
|
|
2
2
|
module NestedValid
|
3
3
|
def to_hash(*)
|
4
4
|
nested_forms do |attr|
|
5
|
-
# attr.delete(:prepare)
|
6
|
-
# attr.delete(:extend)
|
7
|
-
|
8
5
|
attr.merge!(
|
9
6
|
:serialize => lambda { |object, args|
|
10
7
|
|
@@ -13,8 +10,6 @@ module Reform::Contract::Validate
|
|
13
10
|
options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
|
14
11
|
options[:prefix] << args.binding.name # FIXME: should be #as.
|
15
12
|
|
16
|
-
# puts "======= user_options: #{args.user_options.inspect}"
|
17
|
-
|
18
13
|
object.validate!(options) # recursively call valid?
|
19
14
|
},
|
20
15
|
)
|
@@ -34,7 +29,6 @@ module Reform::Contract::Validate
|
|
34
29
|
errors.valid?
|
35
30
|
end
|
36
31
|
def validate!(options)
|
37
|
-
# puts "validate! in #{self.class.name}: #{true.inspect}"
|
38
32
|
prefix = options[:prefix]
|
39
33
|
|
40
34
|
# call valid? recursively and collect nested errors.
|
data/lib/reform/form.rb
CHANGED
@@ -1,15 +1,7 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
|
3
|
-
require 'reform/contract'
|
4
|
-
require 'reform/composition'
|
5
|
-
require 'reform/form/module'
|
6
|
-
|
7
1
|
module Reform
|
8
2
|
class Form < Contract
|
9
3
|
self.representer_class = Reform::Representer.for(:form_class => self)
|
10
4
|
|
11
|
-
require "reform/form/virtual_attributes"
|
12
|
-
|
13
5
|
require 'reform/form/validate'
|
14
6
|
include Validate # extend Contract#validate with additional behaviour.
|
15
7
|
require 'reform/form/sync'
|
@@ -29,5 +21,11 @@ module Reform
|
|
29
21
|
|
30
22
|
require 'reform/form/scalar'
|
31
23
|
extend Scalar::Property # experimental feature!
|
24
|
+
|
25
|
+
|
26
|
+
# DISCUSS: should that be optional? hooks into #validate, too.
|
27
|
+
require 'reform/form/changed'
|
28
|
+
register_feature Changed
|
29
|
+
include Changed
|
32
30
|
end
|
33
31
|
end
|
@@ -10,13 +10,14 @@ module Reform::Form::ActiveModel
|
|
10
10
|
end
|
11
11
|
|
12
12
|
module ClassMethods
|
13
|
-
|
13
|
+
private
|
14
|
+
|
15
|
+
def property(name, options={}, &block)
|
14
16
|
super.tap do |definition|
|
15
17
|
add_nested_attribute_compat(name) if definition[:form] # TODO: fix that in Rails FB#1832 work.
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
19
|
-
private
|
20
21
|
# The Rails FormBuilder "detects" nested attributes (which is what we want) by checking existance of a setter method.
|
21
22
|
def add_nested_attribute_compat(name)
|
22
23
|
define_method("#{name}_attributes=") {} # this is why i hate respond_to? in Rails.
|
@@ -29,12 +29,24 @@ module Reform::Form::ActiveModel
|
|
29
29
|
private
|
30
30
|
|
31
31
|
def add_validator(validator)
|
32
|
+
if validator.respond_to?(:attributes)
|
33
|
+
add_native_validator validator
|
34
|
+
else
|
35
|
+
add_custom_validator validator
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_native_validator validator
|
32
40
|
attributes = inverse_map_attributes(validator.attributes)
|
33
41
|
if attributes.any?
|
34
42
|
@form_class.validates(*attributes, {validator.kind => validator.options})
|
35
43
|
end
|
36
44
|
end
|
37
45
|
|
46
|
+
def add_custom_validator validator
|
47
|
+
@form_class.validates(nil, {validator.kind => validator.options})
|
48
|
+
end
|
49
|
+
|
38
50
|
def inverse_map_attributes(attributes)
|
39
51
|
@mapping.inverse_image(create_attributes(attributes))
|
40
52
|
end
|
@@ -95,4 +107,4 @@ module Reform::Form::ActiveModel
|
|
95
107
|
end
|
96
108
|
|
97
109
|
end
|
98
|
-
end
|
110
|
+
end
|
@@ -35,17 +35,11 @@ module Reform::Form::ActiveRecord
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
|
38
|
+
# TODO: this is no AR thing.
|
39
39
|
def model_for_property(name)
|
40
40
|
return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
|
41
41
|
|
42
42
|
model_name = mapper.representable_attrs.get(name)[:on]
|
43
43
|
model[model_name]
|
44
44
|
end
|
45
|
-
|
46
|
-
# Delegate column for attribute to the model to support simple_form's
|
47
|
-
# attribute type interrogation.
|
48
|
-
def column_for_attribute(name)
|
49
|
-
model_for_property(name).column_for_attribute(name)
|
50
|
-
end
|
51
45
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# ModelReflections will be the interface between the form object and form builders like simple_form.
|
2
|
+
#
|
3
|
+
# This module is meant to collect all dependencies simple_form needs in addition to the ActiveModel ones.
|
4
|
+
# Goal is to collect all methods and define a reflection API so simple_form works with all ORMs and Reform
|
5
|
+
# doesn't have to "guess" what simple_form and other form helpers need.
|
6
|
+
module Reform::Form::ModelReflections
|
7
|
+
def self.included(base)
|
8
|
+
base.register_feature self # makes it work in nested forms.
|
9
|
+
end
|
10
|
+
|
11
|
+
# Delegate column for attribute to the model to support simple_form's
|
12
|
+
# attribute type interrogation.
|
13
|
+
def column_for_attribute(name)
|
14
|
+
model_for_property(name).column_for_attribute(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
# this should also contain to_param and friends as this is used by the form helpers.
|
18
|
+
end
|
data/lib/reform/form/save.rb
CHANGED
@@ -15,17 +15,26 @@ module Reform::Form::Save
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# Returns the result of that save invocation on the model.
|
18
|
-
def save(&block)
|
18
|
+
def save(options={}, &block)
|
19
19
|
# DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
|
20
20
|
return deprecate_first_save_block_arg(&block) if block_given?
|
21
21
|
|
22
22
|
sync_models # recursion
|
23
|
-
save!
|
23
|
+
save!(options)
|
24
24
|
end
|
25
25
|
|
26
|
-
def save!
|
26
|
+
def save!(options={}) # FIXME.
|
27
27
|
result = save_model
|
28
28
|
mapper.new(fields).extend(RecursiveSave).to_hash # save! on all nested forms. # TODO: only include nested forms here.
|
29
|
+
|
30
|
+
names = options.keys & changed.keys.map(&:to_sym)
|
31
|
+
if names.size > 0
|
32
|
+
representer = save_dynamic_representer.new(fields) # should be done once, on class instance level.
|
33
|
+
|
34
|
+
# puts "$$$$$$$$$ #{names.inspect}"
|
35
|
+
representer.to_hash(options.merge :include => names)
|
36
|
+
end
|
37
|
+
|
29
38
|
result
|
30
39
|
end
|
31
40
|
|
@@ -34,6 +43,19 @@ module Reform::Form::Save
|
|
34
43
|
end
|
35
44
|
|
36
45
|
|
46
|
+
def save_dynamic_representer
|
47
|
+
# puts mapper.superclass.superclass.inspect
|
48
|
+
Class.new(mapper).apply do |dfn|
|
49
|
+
dfn.merge!(
|
50
|
+
:serialize => lambda { |object, options|
|
51
|
+
puts "$$ #{options.user_options.inspect}"
|
52
|
+
options.user_options[options.binding.name.to_sym].call(object, options) },
|
53
|
+
:representable => true
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
37
59
|
module NestedHash
|
38
60
|
def to_hash(*)
|
39
61
|
# Transform form data into a nested hash for #save.
|
data/lib/reform/form/scalar.rb
CHANGED
@@ -16,7 +16,7 @@ module Reform::Form::Scalar
|
|
16
16
|
def save!
|
17
17
|
end
|
18
18
|
|
19
|
-
def sync!
|
19
|
+
def sync!(*)
|
20
20
|
model.replace(fields)
|
21
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
22
|
end
|
@@ -41,6 +41,8 @@ module Reform::Form::Scalar
|
|
41
41
|
|
42
42
|
# TODO: change the way i hook into ::property.
|
43
43
|
module Property
|
44
|
+
private
|
45
|
+
|
44
46
|
def property(name, options={}, &block)
|
45
47
|
if options[:scalar]
|
46
48
|
options.merge!(:features => [Reform::Form::Scalar], populate_if_empty: String)
|
@@ -49,4 +51,4 @@ module Reform::Form::Scalar
|
|
49
51
|
super
|
50
52
|
end
|
51
53
|
end
|
52
|
-
end
|
54
|
+
end
|
data/lib/reform/form/sync.rb
CHANGED
@@ -11,7 +11,8 @@ module Reform::Form::Sync
|
|
11
11
|
nested_forms do |attr|
|
12
12
|
attr.merge!(
|
13
13
|
:instance => lambda { |fragment, *| fragment },
|
14
|
-
|
14
|
+
# FIXME: do we allow options for #sync for nested forms?
|
15
|
+
:deserialize => lambda { |object, *| model = object.sync!({}) } # sync! returns the synced model.
|
15
16
|
# representable's :setter will do collection=([..]) or property=(..) for us on the model.
|
16
17
|
)
|
17
18
|
end
|
@@ -20,14 +21,36 @@ module Reform::Form::Sync
|
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
24
|
+
module Setter
|
25
|
+
def from_hash(*)
|
26
|
+
clone_config!
|
27
|
+
|
28
|
+
representable_attrs.each do |dfn|
|
29
|
+
next unless setter = dfn[:sync]
|
30
|
+
|
31
|
+
setter_proc = lambda do |value, options|
|
32
|
+
# puts "~~ #{value}~ #{options.user_options.inspect}"
|
33
|
+
|
34
|
+
if options.binding[:sync] == true
|
35
|
+
options.user_options[options.binding.name.to_sym].call(value, options)
|
36
|
+
next
|
37
|
+
end
|
38
|
+
|
39
|
+
# evaluate the :sync block in form context (should we do that everywhere?).
|
40
|
+
options.user_options[:form].instance_exec(value, options, &setter)
|
41
|
+
end
|
42
|
+
|
43
|
+
dfn.merge!(:setter => setter_proc)
|
44
|
+
end
|
45
|
+
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
23
50
|
# Transforms form input into what actually gets written to model.
|
24
51
|
# output: {title: "Mint Car", hit: <Form>}
|
25
52
|
module InputRepresenter
|
26
|
-
|
27
|
-
# TODO: make dynamic.
|
28
|
-
include Reform::Form::EmptyAttributesOptions
|
29
|
-
include Reform::Form::ReadonlyAttributesOptions
|
30
|
-
|
53
|
+
# receives Representer::Options hash.
|
31
54
|
def to_hash(*)
|
32
55
|
nested_forms do |attr|
|
33
56
|
attr.merge!(
|
@@ -41,20 +64,67 @@ module Reform::Form::Sync
|
|
41
64
|
end
|
42
65
|
|
43
66
|
|
44
|
-
def sync_models
|
45
|
-
sync!
|
67
|
+
def sync_models(options={})
|
68
|
+
sync!(options)
|
46
69
|
end
|
47
70
|
alias_method :sync, :sync_models
|
48
71
|
|
49
72
|
# reading from fields allows using readers in form for presentation
|
50
73
|
# and writers still pass to fields in #validate????
|
51
|
-
def sync! # semi-public.
|
52
|
-
|
74
|
+
def sync!(options) # semi-public.
|
75
|
+
options = Reform::Representer::Options[options.merge(:form => self)] # options local for this form, only.
|
76
|
+
|
77
|
+
input = sync_hash(options)
|
78
|
+
# if aliased_model was a proper Twin, we could do changed? stuff there.
|
79
|
+
# setter_module = Class.new(self.class.representer_class)
|
80
|
+
# setter_module.send :include, Setter
|
53
81
|
|
54
|
-
|
82
|
+
options.delete(:exclude) # TODO: can we use 2 options?
|
55
83
|
|
56
|
-
mapper.new(aliased_model).extend(Writer).from_hash(input) # sync properties to Song.
|
84
|
+
mapper.new(aliased_model).extend(Writer).extend(Setter).from_hash(input, options) # sync properties to Song.
|
57
85
|
|
58
86
|
model
|
59
87
|
end
|
88
|
+
|
89
|
+
private
|
90
|
+
# API: semi-public.
|
91
|
+
module SyncHash
|
92
|
+
# This hash goes into the Writer that writes properties back to the model. It only contains "writeable" attributes.
|
93
|
+
def sync_hash(options)
|
94
|
+
input_representer = mapper.new(fields).extend(InputRepresenter)
|
95
|
+
input_representer.to_hash(options)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
include SyncHash
|
99
|
+
|
100
|
+
|
101
|
+
# Excludes :virtual properties from #sync in this form.
|
102
|
+
module ReadOnly
|
103
|
+
def sync_hash(options)
|
104
|
+
readonly_fields = mapper.fields { |dfn| dfn[:virtual] }
|
105
|
+
|
106
|
+
options.exclude!(readonly_fields.map(&:to_sym))
|
107
|
+
|
108
|
+
super
|
109
|
+
end
|
110
|
+
end
|
111
|
+
include ReadOnly
|
112
|
+
|
113
|
+
|
114
|
+
# This will skip unchanged properties in #sync. To use this for all nested form do as follows.
|
115
|
+
#
|
116
|
+
# class SongForm < Reform::Form
|
117
|
+
# feature Synd::SkipUnchanged
|
118
|
+
module SkipUnchanged
|
119
|
+
def sync_hash(options)
|
120
|
+
# DISCUSS: we currently don't track if nested forms have changed (only their attributes). that's why i include them all here, which
|
121
|
+
# is additional sync work/slightly wrong. solution: allow forms to form.changed? not sure how to do that with collections.
|
122
|
+
scalars = mapper.fields { |dfn| !dfn[:form] }
|
123
|
+
unchanged = scalars - changed.keys
|
124
|
+
|
125
|
+
# exclude unchanged scalars, nested forms and changed scalars still go in here!
|
126
|
+
options.exclude!(unchanged.map(&:to_sym))
|
127
|
+
super
|
128
|
+
end
|
129
|
+
end
|
60
130
|
end
|
data/lib/reform/form/validate.rb
CHANGED
@@ -26,6 +26,16 @@ module Reform::Form::Validate
|
|
26
26
|
# FIXME: solve this with a dedicated Populate Decorator per Form.
|
27
27
|
representable_attrs.each do |attr|
|
28
28
|
attr.merge!(:parse_filter => Representable::Coercion::Coercer.new(attr[:coercion_type])) if attr[:coercion_type]
|
29
|
+
|
30
|
+
attr.merge!(:skip_if => Skip::AllBlank.new) if attr[:skip_if] == :all_blank
|
31
|
+
attr.merge!(:skip_parse => attr[:skip_if]) if attr[:skip_if]
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
representable_attrs.each do |attr|
|
36
|
+
next if attr[:form]
|
37
|
+
|
38
|
+
attr.merge!(:parse_filter => Changed.new)
|
29
39
|
end
|
30
40
|
|
31
41
|
super
|
@@ -73,6 +83,34 @@ module Reform::Form::Validate
|
|
73
83
|
end # PopulateIfEmpty
|
74
84
|
end
|
75
85
|
|
86
|
+
|
87
|
+
module Skip
|
88
|
+
class AllBlank
|
89
|
+
include Uber::Callable
|
90
|
+
|
91
|
+
def call(form, params, options)
|
92
|
+
# TODO: hahahahahaha.
|
93
|
+
properties = options.binding.representer_module.representer_class.representable_attrs[:definitions].keys
|
94
|
+
|
95
|
+
properties.each { |name| params[name].present? and return false }
|
96
|
+
true # skip
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
class Changed
|
103
|
+
def call(fragment, params, options)
|
104
|
+
# options is a Representable::Options object holding all the stakeholders. this is here becaues of pass_options: true.
|
105
|
+
form = options.represented
|
106
|
+
name = options.binding.name
|
107
|
+
|
108
|
+
form.changed[name] = form.send(name) != fragment
|
109
|
+
|
110
|
+
fragment
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
76
114
|
# 1. Populate the form object graph so that each incoming object has a representative form object.
|
77
115
|
# 2. Deserialize. This is wrong and should be done in 1.
|
78
116
|
# 3. Validate the form object graph.
|
data/lib/reform/rails.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'reform/form/active_model'
|
2
2
|
|
3
|
-
require 'reform/
|
3
|
+
require 'reform/active_record' if defined?(ActiveRecord)
|
4
4
|
|
5
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.
|
6
6
|
include Reform::Form::ActiveModel
|
data/lib/reform/representer.rb
CHANGED
@@ -10,41 +10,32 @@ module Reform
|
|
10
10
|
# self.options = {}
|
11
11
|
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def include!(names)
|
18
|
-
self[:include] ||= []
|
19
|
-
self[:include] += names
|
20
|
-
self
|
21
|
-
end
|
22
|
-
|
23
|
-
def exclude!(names)
|
24
|
-
self[:exclude] ||= []
|
25
|
-
self[:exclude] += names
|
26
|
-
self
|
27
|
-
end
|
13
|
+
class Options < ::Hash
|
14
|
+
def include!(names)
|
15
|
+
includes.push(*names) #if names.size > 0
|
16
|
+
self
|
28
17
|
end
|
29
18
|
|
30
|
-
def
|
31
|
-
|
19
|
+
def exclude!(names)
|
20
|
+
excludes.push(*names) #if names.size > 0
|
21
|
+
self
|
32
22
|
end
|
33
23
|
|
34
|
-
def
|
35
|
-
|
24
|
+
def excludes
|
25
|
+
self[:exclude] ||= []
|
36
26
|
end
|
37
27
|
|
38
|
-
def
|
39
|
-
|
28
|
+
def includes
|
29
|
+
self[:include] ||= []
|
40
30
|
end
|
41
31
|
end
|
42
32
|
|
33
|
+
|
43
34
|
include Representable::Hash
|
44
35
|
|
45
36
|
# Returns hash of all property names.
|
46
|
-
def fields
|
47
|
-
representable_attrs.map(&:name)
|
37
|
+
def self.fields(&block)
|
38
|
+
representable_attrs.find_all(&block).map(&:name)
|
48
39
|
end
|
49
40
|
|
50
41
|
def nested_forms(&block)
|