reform 0.2.7 → 1.0.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 +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
|