reform 1.0.4 → 1.1.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.
@@ -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