reform 2.3.0.rc1 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +5 -1
  3. data/.travis.yml +7 -11
  4. data/CHANGES.md +43 -3
  5. data/Gemfile +2 -5
  6. data/ISSUE_TEMPLATE.md +1 -1
  7. data/LICENSE.txt +1 -1
  8. data/README.md +7 -9
  9. data/Rakefile +6 -10
  10. data/lib/reform/contract.rb +7 -7
  11. data/lib/reform/contract/custom_error.rb +41 -0
  12. data/lib/reform/contract/validate.rb +10 -6
  13. data/lib/reform/errors.rb +27 -15
  14. data/lib/reform/form.rb +22 -11
  15. data/lib/reform/form/call.rb +1 -1
  16. data/lib/reform/form/composition.rb +2 -2
  17. data/lib/reform/form/dry.rb +22 -60
  18. data/lib/reform/form/dry/input_hash.rb +37 -0
  19. data/lib/reform/form/populator.rb +9 -11
  20. data/lib/reform/form/prepopulate.rb +3 -2
  21. data/lib/reform/form/validate.rb +19 -12
  22. data/lib/reform/result.rb +36 -9
  23. data/lib/reform/validation.rb +10 -8
  24. data/lib/reform/validation/groups.rb +2 -4
  25. data/lib/reform/version.rb +1 -1
  26. data/reform.gemspec +9 -9
  27. data/test/benchmarking.rb +10 -11
  28. data/test/call_test.rb +8 -8
  29. data/test/changed_test.rb +13 -13
  30. data/test/coercion_test.rb +56 -24
  31. data/test/composition_test.rb +49 -51
  32. data/test/contract/custom_error_test.rb +55 -0
  33. data/test/contract_test.rb +18 -18
  34. data/test/default_test.rb +3 -3
  35. data/test/deserialize_test.rb +14 -17
  36. data/test/docs/validation_test.rb +134 -0
  37. data/test/errors_test.rb +131 -86
  38. data/test/feature_test.rb +9 -11
  39. data/test/fixtures/dry_error_messages.yml +65 -52
  40. data/test/form_option_test.rb +3 -3
  41. data/test/form_test.rb +6 -6
  42. data/test/from_test.rb +17 -21
  43. data/test/inherit_test.rb +28 -35
  44. data/test/module_test.rb +23 -28
  45. data/test/parse_option_test.rb +12 -12
  46. data/test/parse_pipeline_test.rb +3 -3
  47. data/test/populate_test.rb +146 -93
  48. data/test/populator_skip_test.rb +3 -4
  49. data/test/prepopulator_test.rb +20 -21
  50. data/test/read_only_test.rb +12 -1
  51. data/test/readable_test.rb +7 -7
  52. data/test/reform_test.rb +38 -42
  53. data/test/save_test.rb +16 -19
  54. data/test/setup_test.rb +15 -15
  55. data/test/skip_if_test.rb +30 -19
  56. data/test/skip_setter_and_getter_test.rb +8 -9
  57. data/test/test_helper.rb +12 -5
  58. data/test/validate_test.rb +160 -140
  59. data/test/validation/dry_validation_test.rb +407 -236
  60. data/test/validation/result_test.rb +29 -31
  61. data/test/validation_library_provided_test.rb +3 -3
  62. data/test/virtual_test.rb +46 -6
  63. data/test/writeable_test.rb +13 -13
  64. metadata +32 -29
  65. data/test/readonly_test.rb +0 -14
@@ -17,7 +17,7 @@ module Reform::Form::Call
17
17
  end
18
18
 
19
19
  def failure?
20
- ! @success
20
+ !@success
21
21
  end
22
22
  end
23
23
  end
@@ -16,13 +16,13 @@ module Reform::Form::Composition
16
16
  #
17
17
  # class CoverSongForm < Reform::Form
18
18
  # model :song, on: :cover_song
19
- def model(main_model, options={})
19
+ def model(main_model, options = {})
20
20
  super
21
21
 
22
22
  composition_model = options[:on] || main_model
23
23
 
24
24
  # FIXME: this should just delegate to :model as in FB, and the comp would take care of it internally.
25
- [:persisted?, :to_key, :to_param].each do |method|
25
+ %i[persisted? to_key to_param].each do |method|
26
26
  define_method method do
27
27
  model[composition_model].send(method)
28
28
  end
@@ -1,8 +1,14 @@
1
+ gem 'dry-validation', '~> 1.5'
1
2
  require "dry-validation"
2
- require "dry/validation/schema/form"
3
3
  require "reform/validation"
4
+ require "reform/form/dry/input_hash"
5
+
6
+ ::Dry::Validation.load_extensions(:hints)
4
7
 
5
8
  module Reform::Form::Dry
9
+ class Contract < Dry::Validation::Contract
10
+ end
11
+
6
12
  def self.included(includer)
7
13
  includer.send :include, Validations
8
14
  includer.extend Validations::ClassMethods
@@ -19,76 +25,32 @@ module Reform::Form::Dry
19
25
  includer.extend(ClassMethods)
20
26
  end
21
27
 
22
-
23
28
  class Group
24
- def initialize(options = {})
25
- options ||= {}
26
- schema_class = options[:schema] || Dry::Validation::Schema
27
- @validator = Dry::Validation.Schema(schema_class, build: false)
29
+ include InputHash
28
30
 
29
- @schema_inject_params = options[:with] || {}
31
+ def initialize(**options)
32
+ @validator = options.fetch(:contract, Contract)
33
+ @schema_inject_params = options.fetch(:with, {})
30
34
  end
31
35
 
32
- def instance_exec(&block)
33
- @validator = Dry::Validation.Schema(@validator, build: false, &block)
36
+ attr_reader :validator, :schema_inject_params, :block
34
37
 
35
- # inject the keys into the configure block automatically
36
- keys = @schema_inject_params.keys
37
- @validator.class_eval do
38
- configure do
39
- keys.each { |k| option k }
40
- end
41
- end
38
+ def instance_exec(&block)
39
+ @block = block
42
40
  end
43
41
 
44
42
  def call(form)
45
- dynamic_options = {}
46
- dynamic_options[:form] = form if @schema_inject_params[:form]
47
- inject_options = @schema_inject_params.merge(dynamic_options)
48
-
49
- # TODO: only pass submitted values to Schema#call?
50
- dry_result = call_schema(inject_options, input_hash(form))
51
- # dry_messages = dry_result.messages
52
-
53
- return dry_result
54
- reform_errors = Reform::Contract::Errors.new(dry_result) # TODO: dry should be merged here.
55
- end
56
-
57
- private
58
- def call_schema(inject_options, input)
59
- @validator.new(@validator.rules, inject_options).(input)
60
- end
61
-
62
- # if dry_error is a hash rather than an array then it contains
63
- # the messages for a nested property
64
- # these messages need to be added to the correct collection
65
- # objects.
66
-
67
- # collections:
68
- # {0=>{:name=>["must be filled"]}, 1=>{:name=>["must be filled"]}}
69
-
70
- # Objects:
71
- # {:name=>["must be filled"]}
72
- # simply load up the object and attach the message to it
43
+ # when passing options[:schema] the class instance is already created so we just need to call
44
+ # "call"
45
+ return validator.call(input_hash(form)) unless validator.is_a?(Class) && @validator <= ::Dry::Validation::Contract
73
46
 
74
- # we can't use to_nested_hash as it get's messed up by composition.
75
- def input_hash(form)
76
- hash = form.class.nested_hash_representer.new(form).to_hash
77
- symbolize_hash(hash)
47
+ dynamic_options = { form: form }
48
+ inject_options = schema_inject_params.merge(dynamic_options)
49
+ contract.new(inject_options).call(input_hash(form))
78
50
  end
79
51
 
80
- # dry-v needs symbolized keys
81
- # TODO: Don't do this here... Representers??
82
- def symbolize_hash(old_hash)
83
- old_hash.each_with_object({}) { |(k, v), new_hash|
84
- new_hash[k.to_sym] = if v.is_a?(Hash)
85
- symbolize_hash(v)
86
- elsif v.is_a?(Array)
87
- v.map{ |h| h.is_a?(Hash) ? symbolize_hash(h) : h }
88
- else
89
- v
90
- end
91
- }
52
+ def contract
53
+ @contract ||= Class.new(validator, &block)
92
54
  end
93
55
  end
94
56
  end
@@ -0,0 +1,37 @@
1
+ module Reform::Form::Dry
2
+ module InputHash
3
+ private
4
+
5
+ # if dry_error is a hash rather than an array then it contains
6
+ # the messages for a nested property
7
+ # these messages need to be added to the correct collection
8
+ # objects.
9
+
10
+ # collections:
11
+ # {0=>{:name=>["must be filled"]}, 1=>{:name=>["must be filled"]}}
12
+
13
+ # Objects:
14
+ # {:name=>["must be filled"]}
15
+ # simply load up the object and attach the message to it
16
+
17
+ # we can't use to_nested_hash as it get's messed up by composition.
18
+ def input_hash(form)
19
+ hash = form.class.nested_hash_representer.new(form).to_hash
20
+ symbolize_hash(hash)
21
+ end
22
+
23
+ # dry-v needs symbolized keys
24
+ # TODO: Don't do this here... Representers??
25
+ def symbolize_hash(old_hash)
26
+ old_hash.each_with_object({}) do |(k, v), new_hash|
27
+ new_hash[k.to_sym] = if v.is_a?(Hash)
28
+ symbolize_hash(v)
29
+ elsif v.is_a?(Array)
30
+ v.map { |h| h.is_a?(Hash) ? symbolize_hash(h) : h }
31
+ else
32
+ v
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -26,7 +26,8 @@ class Reform::Form::Populator
26
26
  twin
27
27
  end
28
28
 
29
- private
29
+ private
30
+
30
31
  def call!(options)
31
32
  form = options[:represented]
32
33
  @value.(form, options) # Declarative::Option call.
@@ -37,7 +38,7 @@ private
37
38
  end
38
39
 
39
40
  def get(options)
40
- Representable::GetValue.(nil, options)
41
+ Representable::GetValue.(nil, options)
41
42
  end
42
43
 
43
44
  class IfEmpty < self # Populator
@@ -59,7 +60,8 @@ private
59
60
  end
60
61
  end
61
62
 
62
- private
63
+ private
64
+
63
65
  def run!(form, fragment, options)
64
66
  return @user_proc.new if @user_proc.is_a?(Class) # handle populate_if_empty: Class. this excludes using Callables, though.
65
67
 
@@ -75,18 +77,14 @@ private
75
77
 
76
78
  @value.(form, options[:fragment], options[:user_options])
77
79
  end
78
-
79
80
  end
80
81
 
81
82
  # Sync (default) blindly grabs the corresponding form twin and returns it. This might imply that nil is returned,
82
83
  # and in turn #validate! is called on nil.
83
84
  class Sync < self
84
85
  def call!(options)
85
- if options[:binding].array?
86
- return options[:model][options[:index]]
87
- else
88
- options[:model]
89
- end
86
+ return options[:model][options[:index]] if options[:binding].array?
87
+ options[:model]
90
88
  end
91
89
  end
92
90
 
@@ -102,8 +100,8 @@ private
102
100
  # (which population is) to the form.
103
101
  class External
104
102
  def call(input, options)
105
- options[:represented].class.definitions.
106
- get(options[:binding][:name])[:internal_populator].(input, options)
103
+ options[:represented].class.definitions
104
+ .get(options[:binding][:name])[:internal_populator].(input, options)
107
105
  end
108
106
  end
109
107
  end
@@ -1,14 +1,15 @@
1
1
  # prepopulate!(options)
2
2
  # prepopulator: ->(model, user_options)
3
3
  module Reform::Form::Prepopulate
4
- def prepopulate!(options={})
4
+ def prepopulate!(options = {})
5
5
  prepopulate_local!(options) # call #prepopulate! on local properties.
6
6
  prepopulate_nested!(options) # THEN call #prepopulate! on nested forms.
7
7
 
8
8
  self
9
9
  end
10
10
 
11
- private
11
+ private
12
+
12
13
  def prepopulate_local!(options)
13
14
  schema.each do |dfn|
14
15
  next unless block = dfn[:prepopulator]
@@ -5,17 +5,23 @@ module Reform::Form::Validate
5
5
  include Uber::Callable
6
6
 
7
7
  def call(form, options)
8
- params = options[:input]
9
8
  # TODO: Schema should provide property names as plain list.
10
- properties = options[:binding][:nested].definitions.collect { |dfn| dfn[:name] }
9
+ # ensure param keys are strings.
10
+ params = options[:input].each_with_object({}) { |(k, v), hash|
11
+ hash[k.to_s] = v
12
+ }
11
13
 
12
- properties.each { |name| (!params[name].nil? && params[name] != "") and return false }
13
- true # skip
14
+ # return false if any property inputs are populated.
15
+ options[:binding][:nested].definitions.each do |definition|
16
+ value = params[definition.name.to_s]
17
+ return false if (!value.nil? && value != '')
18
+ end
19
+
20
+ true # skip this property
14
21
  end
15
22
  end
16
23
  end
17
24
 
18
-
19
25
  def validate(params)
20
26
  # allow an external deserializer.
21
27
  @input_params = params # we want to store these for access via dry later
@@ -30,27 +36,28 @@ module Reform::Form::Validate
30
36
  deserializer.new(self).from_hash(params)
31
37
  end
32
38
 
39
+ private
33
40
 
34
- private
35
41
  # Meant to return params processable by the representer. This is the hook for munching date fields, etc.
36
42
  def deserialize!(params)
37
43
  # NOTE: it is completely up to the form user how they want to deserialize (e.g. using an external JSON-API representer).
38
- # use the deserializer as an external instance to operate on the Twin API,
39
- # e.g. adding new items in collections using #<< etc.
44
+ # use the deserializer as an external instance to operate on the Twin API,
45
+ # e.g. adding new items in collections using #<< etc.
40
46
  # DISCUSS: using self here will call the form's setters like title= which might be overridden.
41
47
  params
42
48
  end
43
49
 
44
50
  # Default deserializer for hash.
45
51
  # This is input-specific, e.g. Hash, JSON, or XML.
46
- def deserializer!(source=self.class, options={}) # called on top-level, only, for now.
47
- deserializer = Disposable::Rescheme.from(source,
52
+ def deserializer!(source = self.class, options = {}) # called on top-level, only, for now.
53
+ deserializer = Disposable::Rescheme.from(
54
+ source,
48
55
  {
49
56
  include: [Representable::Hash::AllowSymbols, Representable::Hash],
50
57
  superclass: Representable::Decorator,
51
- definitions_from: lambda { |inline| inline.definitions },
58
+ definitions_from: ->(inline) { inline.definitions },
52
59
  options_from: :deserializer,
53
- exclude_options: [:default, :populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
60
+ exclude_options: %i[default populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
54
61
  }.merge(options)
55
62
  )
56
63
 
data/lib/reform/result.rb CHANGED
@@ -1,28 +1,45 @@
1
1
  module Reform
2
2
  class Contract < Disposable::Twin
3
-
4
3
  # Collects all native results of a form of all groups and provides
5
4
  # a unified API: #success?, #errors, #messages, #hints.
6
5
  # #success? returns validity of the branch.
7
6
  class Result
8
- def initialize(results, nested_results=[]) # DISCUSS: do we like this?
7
+ def initialize(results, nested_results = []) # DISCUSS: do we like this?
9
8
  @results = results # native Result objects, e.g. `#<Dry::Validation::Result output={:title=>"Fallout", :composer=>nil} errors={}>`
10
9
  @failure = (results + nested_results).find(&:failure?) # TODO: test nested.
11
10
  end
12
11
 
13
12
  def failure?; @failure end
13
+
14
14
  def success?; !failure? end
15
15
 
16
16
  def errors(*args); filter_for(:errors, *args) end
17
+
17
18
  def messages(*args); filter_for(:messages, *args) end
19
+
18
20
  def hints(*args); filter_for(:hints, *args) end
19
21
 
20
- private
22
+ def add_error(key, error_text)
23
+ CustomError.new(key, error_text, @results)
24
+ end
25
+
26
+ def to_results
27
+ @results
28
+ end
29
+
30
+ private
31
+
32
+ # this doesn't do nested errors (e.g. )
21
33
  def filter_for(method, *args)
22
- @results.collect { |r| r.public_send(method, *args) }
23
- .inject({}) { |hsh, errs| hsh.merge(errs) }
24
- .find_all { |k, v| v.is_a?(Array) } # filter :nested=>{:something=>["too nested!"]} #DISCUSS: do we want that here?
25
- .to_h
34
+ @results.collect { |r| r.public_send(method, *args).to_h }
35
+ .inject({}) { |hah, err| hah.merge(err) { |key, old_v, new_v| (new_v.is_a?(Array) ? (old_v |= new_v) : old_v.merge(new_v)) } }
36
+ .find_all { |k, v| # filter :nested=>{:something=>["too nested!"]} #DISCUSS: do we want that here?
37
+ if v.is_a?(Hash)
38
+ nested_errors = v.select { |attr_key, val| attr_key.is_a?(Integer) && val.is_a?(Array) && val.any? }
39
+ v = nested_errors.to_a if nested_errors.any?
40
+ end
41
+ v.is_a?(Array)
42
+ }.to_h
26
43
  end
27
44
 
28
45
  # Note: this class will be redundant in Reform 3, where the public API
@@ -39,17 +56,27 @@ module Reform
39
56
  def_delegators :@result, :success?, :failure?
40
57
 
41
58
  def errors(*args); traverse_for(:errors, *args) end
59
+
42
60
  def messages(*args); traverse_for(:messages, *args) end
61
+
43
62
  def hints(*args); traverse_for(:hints, *args) end
44
63
 
45
64
  def advance(*path)
46
65
  path = @path + path.compact # remove index if nil.
47
- return if traverse(@result.errors, path) == {}
66
+ traverse = traverse(@result.errors, path)
67
+ # when returns {} is because no errors are found
68
+ # when returns a String is because an error has been found on the main key not in the nested one.
69
+ # Collection with custom rule will return a String here and does not need to be considered
70
+ # as a nested error.
71
+ # when return an Array without an index is same as String but it's a property with a custom rule.
72
+ # Check test/validation/dry_validation_test.rb:685
73
+ return if traverse == {} || traverse.is_a?(String) || (traverse.is_a?(Array) && path.compact.size == 1)
48
74
 
49
75
  Pointer.new(@result, path)
50
76
  end
51
77
 
52
- private
78
+ private
79
+
53
80
  def traverse(hash, path)
54
81
  path.inject(hash) { |errs, segment| errs[segment] || {} } # FIXME. test if all segments present.
55
82
  end
@@ -7,7 +7,7 @@ module Reform::Validation
7
7
  end
8
8
 
9
9
  # DSL.
10
- def validation(name=nil, options={}, &block)
10
+ def validation(name = nil, options = {}, &block)
11
11
  options = deprecate_validation_positional_args(name, options)
12
12
  name = options[:name] # TODO: remove in favor of kw args in 3.0.
13
13
 
@@ -20,19 +20,17 @@ module Reform::Validation
20
20
  def deprecate_validation_positional_args(name, options)
21
21
  if name.is_a?(Symbol)
22
22
  warn "[Reform] Form::validation API is now: validation(name: :default, if:nil, schema:Schema). Please use keyword arguments instead of positional arguments."
23
- return { name: name }.merge(options)
23
+ return {name: name}.merge(options)
24
24
  end
25
25
 
26
- if name.nil?
27
- return { name: :default }.merge(options)
28
- end
26
+ return {name: :default}.merge(options) if name.nil?
29
27
 
30
- { name: :default }.merge(name)
28
+ {name: :default}.merge(name)
31
29
  end
32
30
 
33
31
  def validation_group_class
34
- raise NoValidationLibraryError, 'no validation library loaded. Please include a ' +
35
- 'validation library such as Reform::Form::Dry'
32
+ raise NoValidationLibraryError, "no validation library loaded. Please include a " +
33
+ "validation library such as Reform::Form::Dry"
36
34
  end
37
35
  end
38
36
 
@@ -40,6 +38,10 @@ module Reform::Validation
40
38
  includer.extend(ClassMethods)
41
39
  end
42
40
 
41
+ def valid?
42
+ validate({})
43
+ end
44
+
43
45
  NoValidationLibraryError = Class.new(RuntimeError)
44
46
  end
45
47
 
@@ -23,7 +23,7 @@ module Reform::Validation
23
23
  group
24
24
  end
25
25
 
26
- private
26
+ private
27
27
 
28
28
  def index_for(options)
29
29
  return find_index { |el| el.first == options[:after] } + 1 if options[:after]
@@ -36,7 +36,6 @@ module Reform::Validation
36
36
  cfg[1]
37
37
  end
38
38
 
39
-
40
39
  # Runs all validations groups according to their rules and returns all Result objects.
41
40
  class Validate
42
41
  def self.call(groups, form)
@@ -44,14 +43,13 @@ module Reform::Validation
44
43
 
45
44
  groups.collect do |(name, group, options)|
46
45
  next unless evaluate?(options[:if], results, form)
47
-
48
46
  results[name] = group.(form) # run validation for group. store and collect <Result>.
49
47
  end
50
48
  end
51
49
 
52
50
  def self.evaluate?(depends_on, results, form)
53
51
  return true if depends_on.nil?
54
- return results[depends_on].success? if depends_on.is_a?(Symbol)
52
+ return !results[depends_on].nil? && results[depends_on].success? if depends_on.is_a?(Symbol)
55
53
  form.instance_exec(results, &depends_on)
56
54
  end
57
55
  end