reform 2.0.5 → 2.1.0.rc1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -1
  3. data/CHANGES.md +12 -0
  4. data/Gemfile +12 -2
  5. data/README.md +9 -14
  6. data/Rakefile +1 -1
  7. data/database.sqlite3 +0 -0
  8. data/lib/reform.rb +1 -0
  9. data/lib/reform/contract.rb +13 -20
  10. data/lib/reform/contract/validate.rb +9 -7
  11. data/lib/reform/form.rb +45 -31
  12. data/lib/reform/form/active_model.rb +10 -10
  13. data/lib/reform/form/active_model/form_builder_methods.rb +5 -4
  14. data/lib/reform/form/active_model/model_reflections.rb +2 -2
  15. data/lib/reform/form/active_model/model_validations.rb +3 -3
  16. data/lib/reform/form/active_model/validations.rb +49 -32
  17. data/lib/reform/form/dry.rb +55 -0
  18. data/lib/reform/form/lotus.rb +4 -1
  19. data/lib/reform/form/module.rb +3 -17
  20. data/lib/reform/form/multi_parameter_attributes.rb +0 -9
  21. data/lib/reform/form/populator.rb +72 -30
  22. data/lib/reform/form/validate.rb +19 -43
  23. data/lib/reform/form/validation/unique_validator.rb +39 -6
  24. data/lib/reform/validation.rb +40 -0
  25. data/lib/reform/validation/groups.rb +73 -0
  26. data/lib/reform/version.rb +1 -1
  27. data/reform.gemspec +3 -1
  28. data/test/active_record_test.rb +2 -0
  29. data/test/contract_test.rb +2 -2
  30. data/test/deprecation_test.rb +27 -0
  31. data/test/deserialize_test.rb +29 -8
  32. data/test/dummy/config/locales/en.yml +4 -1
  33. data/test/errors_test.rb +4 -4
  34. data/test/feature_test.rb +2 -2
  35. data/test/fixtures/dry_error_messages.yml +43 -0
  36. data/test/form_builder_test.rb +10 -8
  37. data/test/form_test.rb +1 -36
  38. data/test/inherit_test.rb +20 -8
  39. data/test/module_test.rb +2 -30
  40. data/test/parse_pipeline_test.rb +15 -0
  41. data/test/populate_test.rb +41 -12
  42. data/test/populator_skip_test.rb +28 -0
  43. data/test/reform_test.rb +1 -1
  44. data/test/skip_if_test.rb +10 -3
  45. data/test/test_helper.rb +11 -2
  46. data/test/unique_test.rb +72 -1
  47. data/test/validate_test.rb +6 -7
  48. data/test/validation/activemodel_validation_test.rb +252 -0
  49. data/test/validation/dry_validation_test.rb +330 -0
  50. metadata +63 -10
  51. data/lib/reform/schema.rb +0 -13
@@ -4,23 +4,34 @@ require "uber/delegates"
4
4
 
5
5
  module Reform::Form::ActiveModel
6
6
  # AM::Validations for your form.
7
- #
8
7
  # Provides ::validates, ::validate, #validate, and #valid?.
8
+ #
9
+ # Most of this file contains unnecessary wiring to make ActiveModel's error message magic work.
10
+ # Since Rails still thinks it's a good idea to do things like object.class.human_attribute_name,
11
+ # we have some hacks in here to provide that. If it doesn't work for you, don't blame us.
9
12
  module Validations
10
13
  def self.included(includer)
11
14
  includer.instance_eval do
12
15
  include Reform::Form::ActiveModel
13
- inheritable_attr :validator
14
- self.validator = Class.new(Validator) # the actual validations happen in this instance.
15
16
 
16
17
  class << self
17
18
  extend Uber::Delegates
18
- delegates :validator, :validates, :validate, :validates_with, :validate_with
19
-
20
- # Hooray! Delegate translation back to Reform's Validator class which contains AM::Validations.
21
- delegates :validator, :human_attribute_name, :lookup_ancestors, :i18n_scope # Rails 3.1.
19
+ # # Hooray! Delegate translation back to Reform's Validator class which contains AM::Validations.
20
+ delegates :active_model_really_sucks, :human_attribute_name, :lookup_ancestors, :i18n_scope # Rails 3.1.
21
+
22
+ def validation_group_class
23
+ Group
24
+ end
25
+
26
+ # this is to allow calls like Form::human_attribute_name (note that this is on the CLASS level) to be resolved.
27
+ # those calls happen when adding errors in a custom validation method, which is defined on the form (as an instance method).
28
+ def active_model_really_sucks
29
+ Class.new(Validator).tap do |v|
30
+ v.model_name = model_name
31
+ end
32
+ end
22
33
  end
23
- end
34
+ end # ::included
24
35
  end
25
36
 
26
37
  def build_errors
@@ -33,6 +44,24 @@ module Reform::Form::ActiveModel
33
44
  send(name)
34
45
  end
35
46
 
47
+ class Group
48
+ def initialize
49
+ @validations = Class.new(Reform::Form::ActiveModel::Validations::Validator)
50
+ end
51
+
52
+ extend Uber::Delegates
53
+ delegates :@validations, :validates, :validate, :validates_with, :validate_with
54
+
55
+ def call(fields, errors, form) # FIXME.
56
+ validator = @validations.new(form)
57
+ validator.valid?
58
+
59
+ validator.errors.each do |name, error| # TODO: handle with proper merge, or something. validator.errors is ALWAYS AM::Errors.
60
+ errors.add(name, error)
61
+ end
62
+ end
63
+ end
64
+
36
65
 
37
66
  # Validator is the validatable object. On the class level, we define validations,
38
67
  # on instance, it exposes #valid?.
@@ -43,45 +72,33 @@ module Reform::Form::ActiveModel
43
72
 
44
73
  class << self
45
74
  def model_name
46
- @_active_model_sucks || ActiveModel::Name.new(Reform::Form, nil, "Reform::Form")
75
+ @_active_model_sucks ||= ActiveModel::Name.new(Reform::Form, nil, "Reform::Form")
47
76
  end
48
77
 
49
78
  def model_name=(name)
50
79
  @_active_model_sucks = name
51
80
  end
52
81
 
53
- def clone
54
- Class.new(self)
82
+ def validates(*args, &block)
83
+ super(*Declarative::DeepDup.(args), &block)
84
+ end
85
+
86
+ # Prevent AM:V from mutating the validator class
87
+ def attr_reader(*)
88
+ end
89
+
90
+ def attr_writer(*)
55
91
  end
56
92
  end
57
93
 
58
- def initialize(form, name)
94
+ def initialize(form)
59
95
  super(form)
60
- self.class.model_name = name # one of the many reasons why i will drop support for AM::V in 2.1.
96
+ self.class.model_name = form.model_name # one of the many reasons why i will drop support for AM::V in 2.1. or maybe a bit later.
61
97
  end
62
98
 
63
99
  def method_missing(m, *args, &block)
64
100
  __getobj__.send(m, *args, &block) # send all methods to the form, even privates.
65
101
  end
66
102
  end
67
-
68
- private
69
-
70
- # Needs to be implemented by every validation backend and implements the
71
- # actual validation. See Reform::Form::Lotus, too!
72
- def valid?
73
- # we always pass the model_name into the validator now, so AM:V can do its magic. problem is that
74
- # AM does validator.class.model_name so we have to hack the dynamic model name into the
75
- # Validator class.
76
- validator = self.class.validator.new(self, model_name)
77
- validator.valid? # run the Validations object's validator with the form as context. this won't pollute anything in the form.
78
-
79
- #errors.merge!(validator.errors, "")
80
- validator.errors.each do |name, error| # TODO: handle with proper merge, or something. validator.errors is ALWAYS AM::Errors.
81
- errors.add(name, error)
82
- end
83
-
84
- errors.empty?
85
- end
86
103
  end
87
104
  end
@@ -0,0 +1,55 @@
1
+ require "dry-validation"
2
+ require "reform/validation"
3
+ require "dry/validation/schema/form"
4
+
5
+ module Reform::Form::Dry
6
+ module Validations
7
+
8
+ def build_errors
9
+ Reform::Contract::Errors.new(self)
10
+ end
11
+
12
+ module ClassMethods
13
+ def validation_group_class
14
+ Group
15
+ end
16
+ end
17
+
18
+ def self.included(includer)
19
+ includer.extend(ClassMethods)
20
+ end
21
+
22
+ class Group
23
+ def initialize
24
+ @validator = Class.new(ValidatorSchema)
25
+ end
26
+
27
+ def instance_exec(&block)
28
+ @validator.class_eval(&block)
29
+ end
30
+
31
+ def call(fields, reform_errors, form)
32
+ validator = @validator.new(form)
33
+
34
+ validator.call(fields).messages.each do |dry_error|
35
+ # a dry error message looks like this:
36
+ # [:email, [['Please provide your email', '']]]
37
+ dry_error[1].each do |attr_error|
38
+ reform_errors.add(dry_error[0], attr_error[0])
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ class ValidatorSchema < Dry::Validation::Schema::Form
45
+ def initialize(form)
46
+ @form = form
47
+ super()
48
+ end
49
+
50
+ def form
51
+ @form
52
+ end
53
+ end
54
+ end
55
+ end
@@ -4,11 +4,14 @@ require "lotus/validations"
4
4
  module Reform::Form::Lotus
5
5
  class Errors < Lotus::Validations::Errors
6
6
  def merge!(errors, prefix)
7
+ new_errors = {}
7
8
  errors.instance_variable_get(:@errors).each do |name, err|
8
9
  field = (prefix+[name]).join(".")
9
- add(field, *err) # TODO: use namespace feature in Lotus here!
10
+ new_errors[field] = err
10
11
  end
11
12
  # next if messages[field] and messages[field].include?(msg)
13
+
14
+ new_errors.each { |field, err| add(field, *err) } # TODO: use namespace feature in Lotus here!
12
15
  end
13
16
 
14
17
  def inspect
@@ -1,29 +1,15 @@
1
1
  # Include this in every module that gets further included.
2
- # TODO: this could be implemented in Declarable, as we can use that everywhere.
3
2
  module Reform::Form::Module
4
3
  def self.included(base)
5
4
  base.extend ClassMethods
6
- base.extend Included
7
- end
8
-
9
- module Included # TODO: use representable's inheritance mechanism.
10
- def included(base)
11
- super
12
- instructions.each { |cfg|
13
- args = cfg[1].dup
14
- options = args.extract_options!.dup # we need to duplicate options has as AM::Validations messes it up later.
15
5
 
16
- base.send(cfg[0], *args, options, &cfg[2]) } # property :name, {} do .. end
17
- end
6
+ base.extend Declarative::Heritage::DSL # ::heritage
7
+ base.extend Declarative::Heritage::Included # ::included
18
8
  end
19
9
 
20
10
  module ClassMethods
21
11
  def method_missing(method, *args, &block)
22
- instructions << [method, args, block]
23
- end
24
-
25
- def instructions
26
- @instructions ||= []
12
+ heritage.record(method, *args, &block)
27
13
  end
28
14
  end
29
15
  end
@@ -45,13 +45,4 @@ module Reform::Form::MultiParameterAttributes
45
45
  def deserialize!(params)
46
46
  super DateTimeParamsFilter.new.call(params) # if params.is_a?(Hash) # this currently works for hash, only.
47
47
  end
48
-
49
- # module BuildDefinition
50
- # def build_definition(name, options, &block)
51
- # return super unless options[:multi_params]
52
-
53
- # options[:parse_filter] << DateParamsFilter.new
54
- # super
55
- # end
56
- # end
57
48
  end
@@ -1,7 +1,7 @@
1
1
  # Implements the :populator option.
2
2
  #
3
- # populator: -> (fragment, twin, options)
4
- # populator: -> (fragment, collection[twin], index, options)
3
+ # populator: -> (fragment:, model:, :binding)
4
+ # populator: -> (fragment:, collection:, index:, binding:)
5
5
  #
6
6
  # For collections, the entire collection and the currently deserialised index is passed in.
7
7
  class Reform::Form::Populator
@@ -12,49 +12,66 @@ class Reform::Form::Populator
12
12
  @value = Uber::Options::Value.new(user_proc) # we can now process Callable, procs, :symbol.
13
13
  end
14
14
 
15
- def call(form, fragment, *args)
16
- options = args.last
15
+ def call(input, options)
16
+ model = get(options)
17
+ twin = call!(options.merge(model: model, collection: model))
17
18
 
18
- # FIXME: the optional index parameter SUCKS.
19
- twin = call!(form, fragment, options.binding.get, *args)
19
+ return twin if twin == Representable::Pipeline::Stop
20
20
 
21
21
  # this kinda sucks. the proc may call self.composer = Artist.new, but there's no way we can
22
22
  # return the twin instead of the model from the #composer= setter.
23
- twin = options.binding.get unless options.binding.array?
23
+ twin = get(options) unless options[:binding].array?
24
24
 
25
- # since Populator#call is invoked as :instance, we always need to return a twin/form here.
25
+ # we always need to return a twin/form here so we can call nested.deserialize().
26
26
  handle_fail(twin, options)
27
27
 
28
28
  twin
29
29
  end
30
30
 
31
31
  private
32
- # DISCUSS: this signature could change soon.
33
- # FIXME: the optional index parameter SUCKS.
34
- def call!(form, fragment, model, *args)
35
- # FIXME: use U:::Value.
36
- form.instance_exec(fragment, model, *args, &@user_proc)
32
+ def call!(options)
33
+ form = options[:represented]
34
+
35
+ deprecate_positional_args(form, @user_proc, options) do
36
+ @value.(form, options)
37
+ end
37
38
  end
38
39
 
39
40
  def handle_fail(twin, options)
40
- raise "[Reform] Your :populator did not return a Reform::Form instance for `#{options.binding.name}`." if options.binding[:twin] && !twin.is_a?(Reform::Form)
41
+ raise "[Reform] Your :populator did not return a Reform::Form instance for `#{options[:binding].name}`." if options[:binding][:nested] && !twin.is_a?(Reform::Form)
42
+ end
43
+
44
+ def get(options)
45
+ Representable::GetValue.(nil, options)
46
+ end
47
+
48
+ def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
49
+ arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
50
+ return yield if arity == 1
51
+ warn "[Reform] Positional arguments for :populator and friends are deprecated. Please use ->(options) and enjoy the rest of your day. Learn more at http://trailblazerb.org/gems/reform/upgrading-guide.html#to-21"
52
+ args = []
53
+ args << options[:index] if options[:index]
54
+ args << options[:representable_options]
55
+ form.instance_exec(options[:fragment], options[:model], *args, &proc)
41
56
  end
42
57
 
43
58
 
44
59
  class IfEmpty < self # Populator
45
- # FIXME: the optional index parameter SUCKS.
46
- def call!(form, fragment, twin, *args)
47
- options = args.last
60
+ def call!(options)
61
+ binding, twin, index, fragment = options[:binding], options[:model], options[:index], options[:fragment] # TODO: remove once we drop 2.0.
62
+ form = options[:represented]
48
63
 
49
- if options.binding.array? # FIXME: ifs suck.
50
- index = args.first
64
+ if binding.array?
51
65
  item = twin.original[index] and return item
52
66
 
53
- twin.insert(index, run!(form, fragment, options)) # form.songs.insert(Song.new)
67
+ new_index = [index, twin.count].min # prevents nil items with initially empty/smaller collections and :skip_if's.
68
+ # this means the fragment index and populated nested form index might be different.
69
+
70
+ twin.insert(new_index, run!(form, fragment, options)) # form.songs.insert(Song.new)
54
71
  else
55
72
  return if twin
56
73
 
57
- form.send(options.binding.setter, run!(form, fragment, options)) # form.artist=(Artist.new)
74
+ form.send(binding.setter, run!(form, fragment, options)) # form.artist=(Artist.new)
58
75
  end
59
76
  end
60
77
 
@@ -62,22 +79,47 @@ private
62
79
  def run!(form, fragment, options)
63
80
  return @user_proc.new if @user_proc.is_a?(Class) # handle populate_if_empty: Class. this excludes using Callables, though.
64
81
 
65
- @value.evaluate(form, fragment, options.user_options)
82
+ deprecate_positional_args(form, @user_proc, options) do
83
+ @value.(form, options)
84
+ end
85
+ end
86
+
87
+ def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
88
+ arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
89
+ return yield if arity == 1
90
+ warn "[Reform] Positional arguments for :prepopulate and friends are deprecated. Please use ->(options) and enjoy the rest of your day. Learn more at http://trailblazerb.org/gems/reform/upgrading-guide.html#to-21"
91
+
92
+ @value.(form, options[:fragment], options[:user_options])
66
93
  end
94
+
67
95
  end
68
96
 
69
97
  # Sync (default) blindly grabs the corresponding form twin and returns it. This might imply that nil is returned,
70
98
  # and in turn #validate! is called on nil.
71
99
  class Sync < self
72
- def call!(form, fragment, model, *args)
73
- options = args.last
74
-
75
- if options.binding.array?
76
- index = args.first
77
- return model[index]
100
+ def call!(options)
101
+ if options[:binding].array?
102
+ return options[:model][options[:index]]
78
103
  else
79
- model
104
+ options[:model]
80
105
  end
81
106
  end
82
107
  end
83
- end
108
+
109
+ # This function is added to the deserializer's pipeline.
110
+ #
111
+ # When deserializing, the representer will call this function and thereby delegate the
112
+ # entire population process to the form. The form's :internal_populator will run its
113
+ # :populator option function and return the new/existing form instance.
114
+ # The deserializing representer will then continue on that returned form.
115
+ #
116
+ # Goal of this indirection is to leave all population logic in the form, while the
117
+ # representer really just traverses an incoming document and dispatches business logic
118
+ # (which population is) to the form.
119
+ class External
120
+ def call(input, options)
121
+ options[:represented].class.definitions.
122
+ get(options[:binding][:name])[:internal_populator].(input, options)
123
+ end
124
+ end
125
+ end
@@ -4,9 +4,10 @@ module Reform::Form::Validate
4
4
  class AllBlank
5
5
  include Uber::Callable
6
6
 
7
- def call(form, params, options)
7
+ def call(form, options)
8
+ params = options[:input]
8
9
  # TODO: Schema should provide property names as plain list.
9
- properties = options.binding[:twin].representer_class.representable_attrs[:definitions].keys
10
+ properties = options[:binding][:nested].definitions.collect { |dfn| dfn[:name] }
10
11
 
11
12
  properties.each { |name| params[name].present? and return false }
12
13
  true # skip
@@ -15,20 +16,19 @@ module Reform::Form::Validate
15
16
  end
16
17
 
17
18
 
18
- # 1. Populate the form object graph so that each incoming object has a representative form object.
19
- # 2. Deserialize. This is wrong and should be done in 1.
20
- # 3. Validate the form object graph.
21
19
  def validate(params)
22
- deprecate_update!(params)
23
-
24
20
  # allow an external deserializer.
25
21
  block_given? ? yield(params) : deserialize(params)
26
22
 
27
23
  super() # run the actual validation on self.
28
- # rescue Representable::DeserializeError
29
- # raise DeserializeError.new("[Reform] Deserialize error: You probably called #validate without setting up your nested models. Check https://github.com/apotonick/reform#populating-forms-for-validation on how to use populators.")
30
24
  end
31
25
 
26
+ def deserialize(params)
27
+ params = deserialize!(params)
28
+ deserializer.new(self).from_hash(params)
29
+ end
30
+
31
+ private
32
32
  # Meant to return params processable by the representer. This is the hook for munching date fields, etc.
33
33
  def deserialize!(params)
34
34
  # NOTE: it is completely up to the form user how they want to deserialize (e.g. using an external JSON-API representer).
@@ -38,42 +38,18 @@ module Reform::Form::Validate
38
38
  params
39
39
  end
40
40
 
41
-
42
- private
43
- # Some users use this method to pre-populate a form. Not saying this is right, but we'll keep
44
- # this method here.
45
- # DISCUSS: this is only called once, on the top-level form.
46
- def deprecate_update!(params)
47
- return unless self.class.instance_methods(false).include?(:update!)
48
- warn "[Reform] Form#update! is deprecated and will be removed in Reform 2.1. Please use #prepopulate!"
49
- update!(params)
50
- end
51
-
52
- def deserialize(params)
53
- params = deserialize!(params)
54
-
55
- deserializer.new(self).from_hash(params)
56
- end
57
-
58
41
  # Default deserializer for hash.
59
42
  # This is input-specific, e.g. Hash, JSON, or XML.
60
- def deserializer # called on top-level, only, for now.
61
- deserializer = Disposable::Twin::Schema.from(self.class,
62
- include: [Representable::Hash::AllowSymbols, Representable::Hash],
63
- superclass: Representable::Decorator,
64
- representer_from: lambda { |inline| inline.representer_class },
65
- options_from: :deserializer,
66
- exclude_options: [:default], # Reform must not copy Disposable/Reform-only options that might confuse representable.
67
- ) do |dfn|
68
- # next unless dfn[:twin]
69
- dfn.merge!(
70
- deserialize: lambda { |decorator, params, options|
71
- params = decorator.represented.deserialize!(params) # let them set up params. # FIXME: we could also get a new deserializer here.
72
-
73
- decorator.from_hash(params) # options.binding.deserialize_method.inspect
74
- }
75
- ) if dfn[:twin]
76
- end
43
+ def deserializer(source=self.class, options={}) # called on top-level, only, for now.
44
+ deserializer = Disposable::Rescheme.from(source,
45
+ {
46
+ include: [Representable::Hash::AllowSymbols, Representable::Hash],
47
+ superclass: Representable::Decorator,
48
+ definitions_from: lambda { |inline| inline.definitions },
49
+ options_from: :deserializer,
50
+ exclude_options: [:default, :populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
51
+ }.merge(options)
52
+ )
77
53
 
78
54
  deserializer
79
55
  end