reform 2.3.0.rc1 → 2.3.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +30 -0
  4. data/.rubocop_todo.yml +460 -0
  5. data/.travis.yml +26 -11
  6. data/CHANGES.md +25 -2
  7. data/Gemfile +6 -3
  8. data/ISSUE_TEMPLATE.md +1 -1
  9. data/README.md +2 -4
  10. data/Rakefile +18 -9
  11. data/lib/reform/contract.rb +7 -7
  12. data/lib/reform/contract/custom_error.rb +41 -0
  13. data/lib/reform/contract/validate.rb +9 -5
  14. data/lib/reform/errors.rb +27 -15
  15. data/lib/reform/form.rb +22 -11
  16. data/lib/reform/form/call.rb +1 -1
  17. data/lib/reform/form/composition.rb +2 -2
  18. data/lib/reform/form/dry.rb +10 -86
  19. data/lib/reform/form/dry/input_hash.rb +37 -0
  20. data/lib/reform/form/dry/new_api.rb +58 -0
  21. data/lib/reform/form/dry/old_api.rb +61 -0
  22. data/lib/reform/form/populator.rb +9 -11
  23. data/lib/reform/form/prepopulate.rb +3 -2
  24. data/lib/reform/form/validate.rb +19 -12
  25. data/lib/reform/result.rb +36 -9
  26. data/lib/reform/validation.rb +10 -8
  27. data/lib/reform/validation/groups.rb +2 -3
  28. data/lib/reform/version.rb +1 -1
  29. data/reform.gemspec +10 -9
  30. data/test/benchmarking.rb +10 -11
  31. data/test/call_new_api.rb +23 -0
  32. data/test/{call_test.rb → call_old_api.rb} +3 -3
  33. data/test/changed_test.rb +7 -7
  34. data/test/coercion_test.rb +50 -18
  35. data/test/composition_new_api.rb +186 -0
  36. data/test/{composition_test.rb → composition_old_api.rb} +23 -26
  37. data/test/contract/custom_error_test.rb +55 -0
  38. data/test/contract_new_api.rb +77 -0
  39. data/test/{contract_test.rb → contract_old_api.rb} +8 -8
  40. data/test/default_test.rb +1 -1
  41. data/test/deserialize_test.rb +8 -11
  42. data/test/errors_new_api.rb +225 -0
  43. data/test/errors_old_api.rb +230 -0
  44. data/test/feature_test.rb +7 -9
  45. data/test/fixtures/dry_error_messages.yml +5 -2
  46. data/test/fixtures/dry_new_api_error_messages.yml +104 -0
  47. data/test/form_new_api.rb +57 -0
  48. data/test/{form_test.rb → form_old_api.rb} +2 -2
  49. data/test/form_option_new_api.rb +24 -0
  50. data/test/{form_option_test.rb → form_option_old_api.rb} +1 -1
  51. data/test/from_test.rb +8 -12
  52. data/test/inherit_new_api.rb +105 -0
  53. data/test/{inherit_test.rb → inherit_old_api.rb} +10 -17
  54. data/test/module_new_api.rb +137 -0
  55. data/test/{module_test.rb → module_old_api.rb} +19 -15
  56. data/test/parse_option_test.rb +5 -5
  57. data/test/parse_pipeline_test.rb +2 -2
  58. data/test/populate_new_api.rb +304 -0
  59. data/test/{populate_test.rb → populate_old_api.rb} +28 -34
  60. data/test/populator_skip_test.rb +1 -2
  61. data/test/prepopulator_test.rb +5 -6
  62. data/test/read_only_test.rb +12 -1
  63. data/test/readable_test.rb +5 -5
  64. data/test/reform_new_api.rb +204 -0
  65. data/test/{reform_test.rb → reform_old_api.rb} +17 -23
  66. data/test/save_new_api.rb +101 -0
  67. data/test/{save_test.rb → save_old_api.rb} +10 -13
  68. data/test/setup_test.rb +6 -6
  69. data/test/{skip_if_test.rb → skip_if_new_api.rb} +20 -9
  70. data/test/skip_if_old_api.rb +92 -0
  71. data/test/skip_setter_and_getter_test.rb +2 -3
  72. data/test/test_helper.rb +13 -5
  73. data/test/validate_new_api.rb +408 -0
  74. data/test/{validate_test.rb → validate_old_api.rb} +43 -53
  75. data/test/validation/dry_validation_new_api.rb +826 -0
  76. data/test/validation/{dry_validation_test.rb → dry_validation_old_api.rb} +223 -116
  77. data/test/validation/result_test.rb +20 -22
  78. data/test/validation_library_provided_test.rb +3 -3
  79. data/test/virtual_test.rb +46 -6
  80. data/test/writeable_test.rb +7 -7
  81. metadata +101 -51
  82. data/test/errors_test.rb +0 -180
  83. data/test/readonly_test.rb +0 -14
@@ -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,95 +1,19 @@
1
1
  require "dry-validation"
2
- require "dry/validation/schema/form"
2
+ require "dry/validation/version"
3
3
  require "reform/validation"
4
+ require "reform/form/dry/input_hash"
4
5
 
5
6
  module Reform::Form::Dry
6
7
  def self.included(includer)
7
- includer.send :include, Validations
8
- includer.extend Validations::ClassMethods
9
- end
10
-
11
- module Validations
12
- module ClassMethods
13
- def validation_group_class
14
- Group
15
- end
8
+ if Gem::Version.new(Dry::Validation::VERSION) > Gem::Version.new("0.13.3")
9
+ require "reform/form/dry/new_api"
10
+ validations = Reform::Form::Dry::NewApi::Validations
11
+ else
12
+ require "reform/form/dry/old_api"
13
+ validations = Reform::Form::Dry::OldApi::Validations
16
14
  end
17
15
 
18
- def self.included(includer)
19
- includer.extend(ClassMethods)
20
- end
21
-
22
-
23
- 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)
28
-
29
- @schema_inject_params = options[:with] || {}
30
- end
31
-
32
- def instance_exec(&block)
33
- @validator = Dry::Validation.Schema(@validator, build: false, &block)
34
-
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
42
- end
43
-
44
- 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
73
-
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)
78
- end
79
-
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
- }
92
- end
93
- end
16
+ includer.send :include, validations
17
+ includer.extend validations::ClassMethods
94
18
  end
95
19
  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
@@ -0,0 +1,58 @@
1
+ module Reform::Form::Dry
2
+ module NewApi
3
+ class Contract < Dry::Validation::Contract
4
+ end
5
+
6
+ module Validations
7
+ module ClassMethods
8
+ def validation_group_class
9
+ Group
10
+ end
11
+ end
12
+
13
+ def self.included(includer)
14
+ includer.extend(ClassMethods)
15
+ end
16
+
17
+ class Group
18
+ include InputHash
19
+
20
+ def initialize(options = {})
21
+ options ||= {}
22
+ @validator = options[:schema] || Reform::Form::Dry::NewApi::Contract
23
+
24
+ @schema_inject_params = options[:with] || {}
25
+ end
26
+
27
+ def instance_exec(&block)
28
+ Dry::Validation.load_extensions(:hints)
29
+ @block = block
30
+ end
31
+
32
+ def call(form)
33
+ dynamic_options = {}
34
+ dynamic_options[:form] = form if @schema_inject_params[:form]
35
+ inject_options = @schema_inject_params.merge(dynamic_options)
36
+
37
+ Dry::Schema::DSL.class_eval do
38
+ inject_options.each do |key, value|
39
+ define_method(key) { value }
40
+ end
41
+ end
42
+
43
+ # when passing options[:schema] the class instance is already created so we just need to call
44
+ # "call"
45
+ @validator = @validator.build(&@block) if @validator == Reform::Form::Dry::NewApi::Contract
46
+
47
+ # TODO: only pass submitted values to Schema#call?
48
+ dry_result = @validator.call(input_hash(form))
49
+ # dry_messages = dry_result.messages
50
+
51
+ return dry_result
52
+
53
+ _reform_errors = Reform::Contract::Errors.new(dry_result) # TODO: dry should be merged here.
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ module Reform::Form::Dry
2
+ module OldApi
3
+ class Schema < Dry::Validation::Schema
4
+ end
5
+
6
+ module Validations
7
+ module ClassMethods
8
+ def validation_group_class
9
+ Group
10
+ end
11
+ end
12
+
13
+ def self.included(includer)
14
+ includer.extend(ClassMethods)
15
+ end
16
+
17
+ class Group
18
+ include InputHash
19
+
20
+ def initialize(options = {})
21
+ options ||= {}
22
+ schema_class = options[:schema] || Reform::Form::Dry::OldApi::Schema
23
+ @validator = Dry::Validation.Schema(schema_class, build: false)
24
+
25
+ @schema_inject_params = options[:with] || {}
26
+ end
27
+
28
+ def instance_exec(&block)
29
+ @validator = Dry::Validation.Schema(@validator, build: false, &block)
30
+ # inject the keys into the configure block automatically
31
+ keys = @schema_inject_params.keys
32
+ @validator.class_eval do
33
+ configure do
34
+ keys.each { |k| option k }
35
+ end
36
+ end
37
+ end
38
+
39
+ def call(form)
40
+ dynamic_options = {}
41
+ dynamic_options[:form] = form if @schema_inject_params[:form]
42
+ inject_options = @schema_inject_params.merge(dynamic_options)
43
+
44
+ # TODO: only pass submitted values to Schema#call?
45
+ dry_result = call_schema(inject_options, input_hash(form))
46
+ # dry_messages = dry_result.messages
47
+
48
+ return dry_result
49
+
50
+ _reform_errors = Reform::Contract::Errors.new(dry_result) # TODO: dry should be merged here.
51
+ end
52
+
53
+ private
54
+
55
+ def call_schema(inject_options, input)
56
+ @validator.new(@validator.rules, inject_options).(input)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ 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
 
@@ -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