reform 2.3.0.rc1 → 2.5.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.
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