reform 1.1.1 → 1.2.0.beta1

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.
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)