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