reform 2.3.0.rc1 → 2.3.0.rc2

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 (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