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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +35 -1
  3. data/Gemfile +1 -1
  4. data/README.md +83 -21
  5. data/TODO.md +8 -0
  6. data/database.sqlite3 +0 -0
  7. data/gemfiles/Gemfile.rails-4.0 +1 -0
  8. data/lib/reform.rb +4 -2
  9. data/lib/reform/active_record.rb +2 -1
  10. data/lib/reform/composition.rb +2 -2
  11. data/lib/reform/contract.rb +24 -7
  12. data/lib/reform/contract/setup.rb +21 -9
  13. data/lib/reform/contract/validate.rb +0 -6
  14. data/lib/reform/form.rb +6 -8
  15. data/lib/reform/form/active_model.rb +3 -2
  16. data/lib/reform/form/active_model/model_validations.rb +13 -1
  17. data/lib/reform/form/active_record.rb +1 -7
  18. data/lib/reform/form/changed.rb +9 -0
  19. data/lib/reform/form/json.rb +13 -0
  20. data/lib/reform/form/model_reflections.rb +18 -0
  21. data/lib/reform/form/save.rb +25 -3
  22. data/lib/reform/form/scalar.rb +4 -2
  23. data/lib/reform/form/sync.rb +82 -12
  24. data/lib/reform/form/validate.rb +38 -0
  25. data/lib/reform/rails.rb +1 -1
  26. data/lib/reform/representer.rb +14 -23
  27. data/lib/reform/schema.rb +23 -0
  28. data/lib/reform/twin.rb +20 -0
  29. data/lib/reform/version.rb +1 -1
  30. data/reform.gemspec +2 -2
  31. data/test/active_model_test.rb +2 -2
  32. data/test/active_record_test.rb +7 -4
  33. data/test/changed_test.rb +69 -0
  34. data/test/custom_validation_test.rb +47 -0
  35. data/test/deserialize_test.rb +2 -7
  36. data/test/empty_test.rb +30 -0
  37. data/test/fields_test.rb +24 -0
  38. data/test/form_composition_test.rb +24 -2
  39. data/test/form_test.rb +84 -0
  40. data/test/inherit_test.rb +12 -0
  41. data/test/model_reflections_test.rb +65 -0
  42. data/test/read_only_test.rb +28 -0
  43. data/test/reform_test.rb +2 -175
  44. data/test/representer_test.rb +47 -0
  45. data/test/save_test.rb +51 -1
  46. data/test/scalar_test.rb +0 -18
  47. data/test/skip_if_test.rb +62 -0
  48. data/test/skip_unchanged_test.rb +86 -0
  49. data/test/sync_option_test.rb +83 -0
  50. data/test/twin_test.rb +23 -0
  51. data/test/validate_test.rb +9 -1
  52. metadata +37 -9
  53. 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
- def property(name, options={})
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,9 @@
1
+ module Reform::Form::Changed
2
+ def changed?(name=nil)
3
+ !! changed[name.to_s]
4
+ end
5
+
6
+ def changed
7
+ @changed ||= {}
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ require 'representable/json'
2
+
3
+ module Reform
4
+ module Form::JSON
5
+ def self.included(base)
6
+ base.representer_class.send :include, Representable::JSON
7
+ end
8
+
9
+ def deserialize_method
10
+ :from_json
11
+ end
12
+ end
13
+ 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
@@ -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.
@@ -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
@@ -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
- :deserialize => lambda { |object, *| model = object.sync! } # sync! returns the synced model.
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
- include Reform::Representer::WithOptions
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
- input_representer = mapper.new(fields).extend(InputRepresenter)
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
- input = input_representer.to_hash
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
@@ -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/form/active_record' if defined?(ActiveRecord)
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
@@ -10,41 +10,32 @@ module Reform
10
10
  # self.options = {}
11
11
 
12
12
 
13
- # Invokes #to_hash and/or #from_hash with #options. This provides a hook for other
14
- # modules to add options for the representational process.
15
- module WithOptions
16
- class Options < Hash
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 options
31
- Options.new
19
+ def exclude!(names)
20
+ excludes.push(*names) #if names.size > 0
21
+ self
32
22
  end
33
23
 
34
- def to_hash(*)
35
- super(options)
24
+ def excludes
25
+ self[:exclude] ||= []
36
26
  end
37
27
 
38
- def from_hash(*)
39
- super(options)
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)