reform 0.2.7 → 1.0.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 +18 -0
- data/Gemfile +2 -1
- data/README.md +196 -17
- data/TODO.md +14 -3
- data/database.sqlite3 +0 -0
- data/lib/reform.rb +9 -1
- data/lib/reform/composition.rb +41 -34
- data/lib/reform/contract.rb +109 -0
- data/lib/reform/contract/errors.rb +33 -0
- data/lib/reform/contract/setup.rb +44 -0
- data/lib/reform/contract/validate.rb +48 -0
- data/lib/reform/form.rb +13 -309
- data/lib/reform/form/active_model.rb +8 -5
- data/lib/reform/form/active_record.rb +30 -37
- data/lib/reform/form/coercion.rb +10 -11
- data/lib/reform/form/composition.rb +40 -50
- data/lib/reform/form/multi_parameter_attributes.rb +6 -1
- data/lib/reform/form/save.rb +61 -0
- data/lib/reform/form/sync.rb +60 -0
- data/lib/reform/form/validate.rb +104 -0
- data/lib/reform/form/virtual_attributes.rb +3 -5
- data/lib/reform/representer.rb +17 -3
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +2 -1
- data/test/active_model_test.rb +0 -92
- data/test/as_test.rb +75 -0
- data/test/coercion_test.rb +26 -8
- data/test/composition_test.rb +8 -8
- data/test/contract_test.rb +57 -0
- data/test/errors_test.rb +37 -10
- data/test/feature_test.rb +28 -0
- data/test/form_builder_test.rb +105 -0
- data/test/form_composition_test.rb +30 -13
- data/test/nested_form_test.rb +12 -18
- data/test/reform_test.rb +11 -6
- data/test/save_test.rb +81 -0
- data/test/setup_test.rb +38 -0
- data/test/sync_test.rb +39 -0
- data/test/test_helper.rb +36 -2
- data/test/validate_test.rb +191 -0
- metadata +42 -4
@@ -3,13 +3,15 @@ module Reform::Form::ActiveModel
|
|
3
3
|
def self.included(base)
|
4
4
|
base.class_eval do
|
5
5
|
extend ClassMethods # ::model_name
|
6
|
+
features << FormBuilderMethods
|
6
7
|
end
|
7
8
|
end
|
8
9
|
|
9
10
|
module ClassMethods
|
10
11
|
def property(name, options={})
|
11
|
-
super
|
12
|
-
|
12
|
+
super.tap do |definition|
|
13
|
+
add_nested_attribute_compat(name) if definition[:form] # TODO: fix that in Rails FB#1832 work.
|
14
|
+
end
|
13
15
|
end
|
14
16
|
|
15
17
|
private
|
@@ -20,7 +22,7 @@ module Reform::Form::ActiveModel
|
|
20
22
|
end
|
21
23
|
|
22
24
|
# Modify the incoming Rails params hash to be representable compliant.
|
23
|
-
def
|
25
|
+
def update!(params)
|
24
26
|
# DISCUSS: #validate should actually expect the complete params hash and then pick the right key as it knows the form name.
|
25
27
|
# however, this would cause confusion?
|
26
28
|
mapper.new(self).nested_forms do |attr, model| # FIXME: make this simpler.
|
@@ -36,16 +38,17 @@ module Reform::Form::ActiveModel
|
|
36
38
|
return unless params.has_key?(nested_name)
|
37
39
|
|
38
40
|
value = params["#{attr.name}_attributes"]
|
39
|
-
value = value.values if attr
|
41
|
+
value = value.values if attr[:collection]
|
40
42
|
|
41
43
|
params[attr.name] = value
|
42
44
|
end
|
43
|
-
end
|
45
|
+
end # FormBuilderMethods
|
44
46
|
|
45
47
|
|
46
48
|
def self.included(base)
|
47
49
|
base.class_eval do
|
48
50
|
extend ClassMethods
|
51
|
+
features << ActiveModel
|
49
52
|
|
50
53
|
delegate [:persisted?, :to_key, :to_param, :id] => :model
|
51
54
|
|
@@ -1,50 +1,43 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
extend ClassMethods
|
7
|
-
end
|
1
|
+
module Reform::Form::ActiveRecord
|
2
|
+
def self.included(base)
|
3
|
+
base.class_eval do
|
4
|
+
include Reform::Form::ActiveModel
|
5
|
+
extend ClassMethods
|
8
6
|
end
|
7
|
+
end
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
9
|
+
module ClassMethods
|
10
|
+
def validates_uniqueness_of(attribute)
|
11
|
+
validates_with UniquenessValidator, :attributes => [attribute]
|
12
|
+
end
|
13
|
+
def i18n_scope
|
14
|
+
:activerecord
|
17
15
|
end
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
class UniquenessValidator < ::ActiveRecord::Validations::UniquenessValidator
|
19
|
+
# when calling validates it should create the Vali instance already and set @klass there! # TODO: fix this in AM.
|
20
|
+
def validate(form)
|
21
|
+
property = attributes.first
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
# here is the thing: why does AM::UniquenessValidator require a filled-out record to work properly? also, why do we need to set
|
24
|
+
# the class? it would be way easier to pass #validate a hash of attributes and get back an errors hash.
|
25
|
+
# the class for the finder could either be infered from the record or set in the validator instance itself in the call to ::validates.
|
26
|
+
record = form.model_for_property(property)
|
27
|
+
record.send("#{property}=", form.send(property))
|
29
28
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
29
|
+
@klass = record.class # this is usually done in the super-sucky #setup method.
|
30
|
+
super(record).tap do |res|
|
31
|
+
form.errors.add(property, record.errors.first.last) if record.errors.present?
|
34
32
|
end
|
35
33
|
end
|
34
|
+
end
|
36
35
|
|
37
|
-
def save(*)
|
38
|
-
super.tap do
|
39
|
-
model.save unless block_given? # DISCUSS: should we implement nested saving here?
|
40
|
-
end
|
41
|
-
end
|
42
36
|
|
43
|
-
|
44
|
-
|
37
|
+
def model_for_property(name)
|
38
|
+
return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
|
45
39
|
|
46
|
-
|
47
|
-
|
48
|
-
end
|
40
|
+
model_name = mapper.representable_attrs[name][:on]
|
41
|
+
send(model_name)
|
49
42
|
end
|
50
43
|
end
|
data/lib/reform/form/coercion.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
require 'representable/decorator/coercion'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module Reform::Form::Coercion
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.features << self
|
7
|
+
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
9
|
+
module ClassMethods
|
10
|
+
def representer_class
|
11
|
+
super.class_eval do
|
12
|
+
include Representable::Decorator::Coercion unless self < Representable::Decorator::Coercion # DISCUSS: include it once. why do we have to check this?
|
13
|
+
self
|
15
14
|
end
|
16
15
|
end
|
17
16
|
end
|
@@ -1,70 +1,60 @@
|
|
1
1
|
require "reform/form/active_model"
|
2
2
|
|
3
|
-
|
3
|
+
module Reform::Form::Composition
|
4
4
|
# Automatically creates a Composition object for you when initializing the form.
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
extend ClassMethods
|
10
|
-
end
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
extend Reform::Form::ActiveModel::ClassMethods # ::model.
|
8
|
+
extend ClassMethods
|
11
9
|
end
|
10
|
+
end
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
module ClassMethods
|
13
|
+
#include Reform::Form::ActiveModel::ClassMethods # ::model.
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
map_from rpr
|
20
|
-
end
|
21
|
-
end
|
15
|
+
def model_class # DISCUSS: needed?
|
16
|
+
@model_class ||= Reform::Composition.from(representer_class)
|
17
|
+
end
|
22
18
|
|
23
|
-
|
24
|
-
|
25
|
-
delegate options[:on] => :@model
|
19
|
+
def property(name, options={})
|
20
|
+
super.tap do |definition|
|
21
|
+
delegate options[:on] => :@model # form.band -> composition.band
|
26
22
|
end
|
23
|
+
end
|
27
24
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
composition_model = options[:on] || main_model
|
25
|
+
# Same as ActiveModel::model but allows you to define the main model in the composition
|
26
|
+
# using +:on+.
|
27
|
+
#
|
28
|
+
# class CoverSongForm < Reform::Form
|
29
|
+
# model :song, on: :cover_song
|
30
|
+
def model(main_model, options={})
|
31
|
+
super
|
37
32
|
|
38
|
-
|
33
|
+
composition_model = options[:on] || main_model
|
39
34
|
|
40
|
-
|
41
|
-
delegate [:persisted?, :to_key, :to_param] => composition_model # #to_key => song.to_key
|
35
|
+
delegate composition_model => :model # #song => model.song
|
42
36
|
|
43
|
-
|
44
|
-
|
45
|
-
end
|
37
|
+
# FIXME: this should just delegate to :model as in FB, and the comp would take care of it internally.
|
38
|
+
delegate [:persisted?, :to_key, :to_param] => composition_model # #to_key => song.to_key
|
46
39
|
|
47
|
-
|
48
|
-
composition = self.class.model_class.new(models)
|
49
|
-
super(composition)
|
50
|
-
end
|
51
|
-
|
52
|
-
def to_nested_hash
|
53
|
-
model.nested_hash_for(to_hash) # use composition to compute nested hash.
|
40
|
+
alias_method main_model, composition_model # #hit => model.song.
|
54
41
|
end
|
55
42
|
end
|
56
43
|
|
44
|
+
def initialize(models)
|
45
|
+
composition = self.class.model_class.new(models)
|
46
|
+
super(composition)
|
47
|
+
end
|
57
48
|
|
58
|
-
#
|
59
|
-
|
60
|
-
|
49
|
+
def aliased_model # we don't need an Expose as we save the Composition instance in the constructor.
|
50
|
+
model
|
51
|
+
end
|
61
52
|
|
62
|
-
|
63
|
-
|
53
|
+
def to_nested_hash
|
54
|
+
model.nested_hash_for(to_hash) # use composition to compute nested hash.
|
55
|
+
end
|
64
56
|
|
65
|
-
|
66
|
-
|
67
|
-
end
|
68
|
-
end
|
57
|
+
def to_hash(*args)
|
58
|
+
mapper.new(self).to_hash(*args)
|
69
59
|
end
|
70
|
-
end
|
60
|
+
end
|
@@ -1,5 +1,9 @@
|
|
1
|
-
|
1
|
+
Reform::Form.class_eval do
|
2
2
|
module MultiParameterAttributes
|
3
|
+
def self.included(base)
|
4
|
+
base.features << self
|
5
|
+
end
|
6
|
+
|
3
7
|
class DateParamsFilter
|
4
8
|
def call(params)
|
5
9
|
date_attributes = {}
|
@@ -33,6 +37,7 @@ class Reform::Form
|
|
33
37
|
|
34
38
|
def validate(params)
|
35
39
|
# TODO: make it cleaner to hook into essential reform steps.
|
40
|
+
# TODO: test with nested.
|
36
41
|
DateParamsFilter.new.call(params)
|
37
42
|
|
38
43
|
super
|
@@ -0,0 +1,61 @@
|
|
1
|
+
Reform::Form.class_eval do
|
2
|
+
module Save
|
3
|
+
module RecursiveSave
|
4
|
+
def to_hash(*)
|
5
|
+
# process output from InputRepresenter {title: "Mint Car", hit: <Form>}
|
6
|
+
# and just call sync! on nested forms.
|
7
|
+
nested_forms do |attr|
|
8
|
+
attr.merge!(
|
9
|
+
:instance => lambda { |fragment, *| fragment },
|
10
|
+
:serialize => lambda { |object, args| object.save! unless args.binding[:save] === false },
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def save
|
19
|
+
# DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
|
20
|
+
return yield self, to_nested_hash if block_given?
|
21
|
+
|
22
|
+
sync_models # recursion
|
23
|
+
save!
|
24
|
+
end
|
25
|
+
|
26
|
+
def save!
|
27
|
+
save_model
|
28
|
+
mapper.new(self).extend(RecursiveSave).to_hash # save! on all nested forms.
|
29
|
+
end
|
30
|
+
|
31
|
+
def save_model
|
32
|
+
model.save # TODO: implement nested (that should really be done by Twin/AR).
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
module NestedHash
|
37
|
+
def to_hash(*)
|
38
|
+
# Transform form data into a nested hash for #save.
|
39
|
+
nested_forms do |attr|
|
40
|
+
attr.merge!(
|
41
|
+
:instance => lambda { |fragment, *| fragment },
|
42
|
+
:serialize => lambda { |object, args| object.to_nested_hash },
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
representable_attrs.each do |attr|
|
47
|
+
attr.merge!(:as => attr[:private_name] || attr.name)
|
48
|
+
end
|
49
|
+
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
|
55
|
+
def to_nested_hash
|
56
|
+
map = mapper.new(self).extend(Save::NestedHash)
|
57
|
+
|
58
|
+
ActiveSupport::HashWithIndifferentAccess.new(map.to_hash)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
Reform::Form.class_eval do
|
2
|
+
# #sync!
|
3
|
+
# 1. assign scalars to model (respecting virtual, excluded attributes)
|
4
|
+
# 2. call sync! on nested
|
5
|
+
module Sync
|
6
|
+
# Mechanics for writing input to model.
|
7
|
+
# Writes input to model.
|
8
|
+
module Writer
|
9
|
+
def from_hash(*)
|
10
|
+
# process output from InputRepresenter {title: "Mint Car", hit: <Form>}
|
11
|
+
# and just call sync! on nested forms.
|
12
|
+
nested_forms do |attr|
|
13
|
+
attr.merge!(
|
14
|
+
:instance => lambda { |fragment, *| fragment },
|
15
|
+
:deserialize => lambda { |object, *| object.sync! },
|
16
|
+
:setter => lambda { |*| } # don't write hit=<Form>.
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Transforms form input into what actually gets written to model.
|
25
|
+
# output: {title: "Mint Car", hit: <Form>}
|
26
|
+
module InputRepresenter
|
27
|
+
include Reform::Representer::WithOptions
|
28
|
+
# TODO: make dynamic.
|
29
|
+
include Reform::Form::EmptyAttributesOptions
|
30
|
+
include Reform::Form::ReadonlyAttributesOptions
|
31
|
+
|
32
|
+
def to_hash(*)
|
33
|
+
nested_forms do |attr|
|
34
|
+
attr.merge!(
|
35
|
+
:representable => false,
|
36
|
+
:prepare => lambda { |obj, *| obj }
|
37
|
+
)
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
### TODO: add ToHash with :prepare => lambda { |form, args| form },
|
47
|
+
|
48
|
+
def sync_models
|
49
|
+
sync!
|
50
|
+
end
|
51
|
+
alias_method :sync, :sync_models
|
52
|
+
|
53
|
+
def sync! # semi-public.
|
54
|
+
input_representer = mapper.new(self).extend(Sync::InputRepresenter)
|
55
|
+
|
56
|
+
input = input_representer.to_hash
|
57
|
+
|
58
|
+
mapper.new(aliased_model).extend(Sync::Writer).from_hash(input)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# Mechanics for writing to forms in #validate.
|
2
|
+
module Reform::Form::Validate
|
3
|
+
module Update
|
4
|
+
def from_hash(*)
|
5
|
+
nested_forms do |attr|
|
6
|
+
attr.delete(:prepare)
|
7
|
+
attr.delete(:extend)
|
8
|
+
|
9
|
+
attr.merge!(
|
10
|
+
:collection => attr[:collection], # TODO: Def#merge! doesn't consider :collection if it's already set in attr YET.
|
11
|
+
:parse_strategy => :sync, # just use nested objects as they are.
|
12
|
+
:deserialize => lambda { |object, params, args| object.update!(params) },
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
module Populator
|
22
|
+
class PopulateIfEmpty
|
23
|
+
def initialize(*args)
|
24
|
+
@form, @fragment, args = args
|
25
|
+
@index = args.first
|
26
|
+
@args = args.last
|
27
|
+
end
|
28
|
+
|
29
|
+
def call
|
30
|
+
binding = @args.binding
|
31
|
+
form = binding.get
|
32
|
+
|
33
|
+
return if binding.array? and form and form[@index] # TODO: this should be handled by the Binding.
|
34
|
+
return if !binding.array? and form
|
35
|
+
# only get here when above form is nil.
|
36
|
+
|
37
|
+
if binding[:populate_if_empty].is_a?(Proc)
|
38
|
+
model = @form.instance_exec(@fragment, @args, &binding[:populate_if_empty]) # call user block.
|
39
|
+
else
|
40
|
+
model = binding[:populate_if_empty].new
|
41
|
+
end
|
42
|
+
|
43
|
+
form = binding[:form].new(model) # free service: wrap model with Form. this usually happens in #setup.
|
44
|
+
|
45
|
+
if binding.array?
|
46
|
+
@form.model.send("#{binding.getter}") << model # FIXME: i don't like this, but we have to add the model to the parent object to make associating work. i have to use #<< to stay compatible with AR's has_many API. DISCUSS: what happens when we get out-of-sync here?
|
47
|
+
@form.send("#{binding.getter}")[@index] = form
|
48
|
+
else
|
49
|
+
@form.model.send("#{binding.setter}", model) # FIXME: i don't like this, but we have to add the model to the parent object to make associating work.
|
50
|
+
@form.send("#{binding.setter}", form) # :setter is currently overwritten by :parse_strategy.
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end # PopulateIfEmpty
|
54
|
+
|
55
|
+
|
56
|
+
def from_hash(params, *args)
|
57
|
+
populated_attrs = []
|
58
|
+
|
59
|
+
nested_forms do |attr|
|
60
|
+
next unless attr[:populate_if_empty]
|
61
|
+
|
62
|
+
attr.merge!(
|
63
|
+
# DISCUSS: it would be cool to move the lambda block to PopulateIfEmpty#call.
|
64
|
+
:populator => lambda do |fragment, *args|
|
65
|
+
PopulateIfEmpty.new(self, fragment, args).call
|
66
|
+
end
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
nested_forms do |attr|
|
72
|
+
next unless attr[:populator]
|
73
|
+
|
74
|
+
attr.merge!(
|
75
|
+
:parse_strategy => attr[:populator],
|
76
|
+
:representable => false
|
77
|
+
)
|
78
|
+
populated_attrs << attr.name.to_sym
|
79
|
+
end
|
80
|
+
|
81
|
+
super(params, {:include => populated_attrs})
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
def validate(params)
|
87
|
+
update!(params)
|
88
|
+
|
89
|
+
super()
|
90
|
+
end
|
91
|
+
|
92
|
+
def update!(params)
|
93
|
+
# puts "updating in #{self.class.name}"
|
94
|
+
populate!(params)
|
95
|
+
|
96
|
+
mapper.new(self).extend(Update).from_hash(params)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def populate!(params)
|
101
|
+
mapper.new(self).extend(Populator).from_hash(params)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|