reform 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.features << self
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
@@ -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 yield self, to_nested_hash if block_given?
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
- :instance => lambda { |fragment, *| fragment },
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
@@ -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 => lambda { |*| } # don't write hit=<Form>.
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
@@ -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
- def initialize(*args)
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
- binding = @args.binding
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 = @args.user_options[: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
- return if binding.array? and form and form[@index] # TODO: this should be handled by the Binding.
37
- return if !binding.array? and form
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(@fragment, @args, &binding[:populate_if_empty]) # call user block.
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
- 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?
50
- @fields.send("#{binding.getter}")[@index] = form
62
+ fields.send("#{binding.getter}")[index] = form
51
63
  else
52
- 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.
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
- mapper.new(self).extend(Update).from_hash(params)
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
@@ -1,9 +1,9 @@
1
1
  require 'reform/form/active_model'
2
- if defined?(ActiveRecord)
3
- require 'reform/form/active_record'
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
@@ -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
- def self.inline_representer(base_module, name, options, &block)
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
- Class.new(self.options[:form_class]) do
77
- # TODO: this will soon become a generic feature in representable.
78
- include *options[:features].reverse if options[:features]
82
+ features = options[:features]
83
+
84
+ Class.new(base || default_inline_class) do
85
+ include *features
79
86
 
80
- instance_exec &block
87
+ class_eval &block
81
88
 
82
89
  @form_name = name
83
90
 
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "1.0.4"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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", "~> 1.8.1"
22
- spec.add_dependency "disposable", "~> 0.0.4"
23
- spec.add_dependency "uber", "~> 0.0.4"
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", ">= 10.1.0"
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
@@ -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 "main form reader #hit" do# TODO: remove in 1.2. we don't support this reader #hit anymore.
114
- it "delegates to :on model" do
115
- form.hit.must_equal rio # TODO: remove in 1.2.
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