reform 2.2.4 → 2.3.1

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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +5 -1
  3. data/.rubocop.yml +30 -0
  4. data/.rubocop_todo.yml +460 -0
  5. data/.travis.yml +11 -6
  6. data/Appraisals +8 -0
  7. data/CHANGES.md +54 -4
  8. data/CONTRIBUTING.md +31 -0
  9. data/Gemfile +2 -16
  10. data/ISSUE_TEMPLATE.md +25 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +5 -7
  13. data/Rakefile +18 -9
  14. data/gemfiles/0.13.0.gemfile +8 -0
  15. data/gemfiles/1.5.0.gemfile +9 -0
  16. data/lib/reform.rb +1 -0
  17. data/lib/reform/contract.rb +7 -17
  18. data/lib/reform/contract/custom_error.rb +41 -0
  19. data/lib/reform/contract/validate.rb +53 -23
  20. data/lib/reform/errors.rb +61 -0
  21. data/lib/reform/form.rb +36 -10
  22. data/lib/reform/form/call.rb +1 -1
  23. data/lib/reform/form/composition.rb +2 -2
  24. data/lib/reform/form/dry.rb +10 -58
  25. data/lib/reform/form/dry/input_hash.rb +37 -0
  26. data/lib/reform/form/dry/new_api.rb +46 -0
  27. data/lib/reform/form/dry/old_api.rb +61 -0
  28. data/lib/reform/form/populator.rb +11 -27
  29. data/lib/reform/form/prepopulate.rb +4 -3
  30. data/lib/reform/form/validate.rb +28 -13
  31. data/lib/reform/result.rb +90 -0
  32. data/lib/reform/validation.rb +19 -11
  33. data/lib/reform/validation/groups.rb +12 -27
  34. data/lib/reform/version.rb +1 -1
  35. data/reform.gemspec +15 -13
  36. data/test/benchmarking.rb +39 -6
  37. data/test/call_new_api.rb +23 -0
  38. data/test/{call_test.rb → call_old_api.rb} +4 -4
  39. data/test/changed_test.rb +8 -8
  40. data/test/coercion_test.rb +51 -19
  41. data/test/composition_new_api.rb +186 -0
  42. data/test/{composition_test.rb → composition_old_api.rb} +66 -31
  43. data/test/contract/custom_error_test.rb +55 -0
  44. data/test/contract_new_api.rb +77 -0
  45. data/test/{contract_test.rb → contract_old_api.rb} +13 -13
  46. data/test/default_test.rb +2 -2
  47. data/test/deserialize_test.rb +11 -14
  48. data/test/errors_new_api.rb +225 -0
  49. data/test/errors_old_api.rb +230 -0
  50. data/test/feature_test.rb +8 -10
  51. data/test/fixtures/dry_error_messages.yml +73 -23
  52. data/test/fixtures/dry_new_api_error_messages.yml +104 -0
  53. data/test/form_new_api.rb +57 -0
  54. data/test/{form_test.rb → form_old_api.rb} +5 -5
  55. data/test/form_option_new_api.rb +24 -0
  56. data/test/{form_option_test.rb → form_option_old_api.rb} +4 -4
  57. data/test/from_test.rb +9 -13
  58. data/test/inherit_new_api.rb +105 -0
  59. data/test/inherit_old_api.rb +105 -0
  60. data/test/{module_test.rb → module_new_api.rb} +20 -25
  61. data/test/module_old_api.rb +146 -0
  62. data/test/parse_option_test.rb +40 -0
  63. data/test/parse_pipeline_test.rb +3 -3
  64. data/test/populate_new_api.rb +304 -0
  65. data/test/{populate_test.rb → populate_old_api.rb} +83 -49
  66. data/test/populator_skip_test.rb +9 -9
  67. data/test/prepopulator_test.rb +8 -9
  68. data/test/read_only_test.rb +12 -1
  69. data/test/readable_test.rb +7 -7
  70. data/test/reform_new_api.rb +204 -0
  71. data/test/{reform_test.rb → reform_old_api.rb} +30 -51
  72. data/test/save_new_api.rb +101 -0
  73. data/test/{save_test.rb → save_old_api.rb} +32 -20
  74. data/test/setup_test.rb +8 -8
  75. data/test/{skip_if_test.rb → skip_if_new_api.rb} +23 -12
  76. data/test/skip_if_old_api.rb +92 -0
  77. data/test/skip_setter_and_getter_test.rb +3 -4
  78. data/test/test_helper.rb +25 -14
  79. data/test/validate_new_api.rb +408 -0
  80. data/test/{validate_test.rb → validate_old_api.rb} +59 -69
  81. data/test/validation/dry_validation_new_api.rb +836 -0
  82. data/test/validation/dry_validation_old_api.rb +772 -0
  83. data/test/validation/result_test.rb +77 -0
  84. data/test/validation_library_provided_test.rb +16 -0
  85. data/test/virtual_test.rb +47 -7
  86. data/test/writeable_test.rb +35 -6
  87. metadata +127 -56
  88. data/gemfiles/Gemfile.disposable-0.3 +0 -6
  89. data/lib/reform/contract/errors.rb +0 -43
  90. data/lib/reform/form/mongoid.rb +0 -37
  91. data/lib/reform/form/orm.rb +0 -26
  92. data/lib/reform/mongoid.rb +0 -4
  93. data/test/deprecation_test.rb +0 -27
  94. data/test/errors_test.rb +0 -165
  95. data/test/inherit_test.rb +0 -119
  96. data/test/readonly_test.rb +0 -14
  97. data/test/validation/dry_test.rb +0 -60
  98. data/test/validation/dry_validation_test.rb +0 -352
  99. data/test/validation/errors.yml +0 -4
@@ -0,0 +1,61 @@
1
+ # Provides the old API for Rails and friends.
2
+ # Note that this might become an optional "deprecation" gem in Reform 3.
3
+ class Reform::Contract::Result::Errors
4
+ def initialize(result, form)
5
+ @result = result # DISCUSS: we don't use this ATM?
6
+ @form = form
7
+ @dotted_errors = {} # Reform does not endorse this style of error msgs.
8
+
9
+ DottedErrors.(@form, [], @dotted_errors)
10
+ end
11
+
12
+ # PROTOTYPING. THIS WILL GO TO A SEPARATE GEM IN REFORM 2.4/3.0.
13
+ DottedErrors = ->(form, prefix, hash) do
14
+ result = form.to_result
15
+ result.messages.collect { |k, v| hash[[*prefix, k].join(".").to_sym] = v }
16
+
17
+ form.schema.each(twin: true) do |dfn|
18
+ Disposable::Twin::PropertyProcessor.new(dfn, form).() do |frm, i|
19
+ form_obj = i ? form.send(dfn[:name])[i] : form.send(dfn[:name])
20
+ DottedErrors.(form_obj, [*prefix, dfn[:name]], hash)
21
+ end
22
+ end
23
+ end
24
+
25
+ def messages(*args)
26
+ @dotted_errors
27
+ end
28
+
29
+ def full_messages
30
+ @dotted_errors.collect { |path, errors|
31
+ human_field = path.to_s.gsub(/([\.\_])+/, " ").gsub(/(\b\w)+/) { |s| s.capitalize }
32
+ errors.collect { |message| "#{human_field} #{message}" }
33
+ }.flatten
34
+ end
35
+
36
+ def [](name)
37
+ @dotted_errors[name] || []
38
+ end
39
+
40
+ def size
41
+ messages.size
42
+ end
43
+
44
+ # needed for rails form helpers
45
+ def empty?
46
+ messages.empty?
47
+ end
48
+
49
+ # we need to delegate adding error to result because every time we call form.errors
50
+ # a new instance of this class is created so we need to update the @results array
51
+ # to be able to add custom errors here.
52
+ # This method will actually work only AFTER a validate call has been made
53
+ def add(key, error_test)
54
+ @result.add_error(key, error_test)
55
+ end
56
+ end
57
+
58
+ # Ensure that we can return Active Record compliant full messages when using dry
59
+ # we only want unique messages in our array
60
+ #
61
+ # @full_errors.add()
@@ -1,5 +1,7 @@
1
1
  module Reform
2
2
  class Form < Contract
3
+ class InvalidOptionsCombinationError < StandardError; end
4
+
3
5
  def self.default_nested_class
4
6
  Form
5
7
  end
@@ -15,8 +17,32 @@ module Reform
15
17
 
16
18
  module Property
17
19
  # Add macro logic, e.g. for :populator.
18
- def property(name, options={}, &block)
19
- definition = super # let representable sort out inheriting of properties, and so on.
20
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
21
+ def property(name, options = {}, &block)
22
+ if (options.keys & %i[skip_if populator]).size == 2
23
+ raise InvalidOptionsCombinationError.new(
24
+ "[Reform] #{self}:property:#{name} Do not use skip_if and populator together, use populator with skip! instead"
25
+ )
26
+ end
27
+
28
+ # if composition and inherited we also need this setting
29
+ # to correctly inherit modules
30
+ options[:_inherited] = options[:inherit] if options.key?(:on) && options.key?(:inherit)
31
+
32
+ if options.key?(:parse)
33
+ options[:deserializer] ||= {}
34
+ options[:deserializer][:writeable] = options.delete(:parse)
35
+ end
36
+
37
+ options[:writeable] ||= options.delete(:writable) if options.key?(:writable)
38
+
39
+ # for virtual collection we need at least to have the collection equal to [] to
40
+ # avoid issue when the populator
41
+ if (options.keys & %i[collection virtual]).size == 2
42
+ options = { default: [] }.merge(options)
43
+ end
44
+
45
+ definition = super # letdisposable and declarative gems sort out inheriting of properties, and so on.
20
46
  definition.merge!(deserializer: {}) unless definition[:deserializer] # always keep :deserializer per property.
21
47
 
22
48
  deserializer_options = definition[:deserializer]
@@ -33,10 +59,10 @@ module Reform
33
59
  external_populator = Populator::External.new
34
60
 
35
61
  # always compute a parse_pipeline for each property of the deserializer and inject it via :parse_pipeline.
36
- # first, let representable compute the pipeline functions by invoking #parse_functions.
62
+ # first, letrepresentable compute the pipeline functions by invoking #parse_functions.
37
63
  if definition[:nested]
38
- parse_pipeline = ->(input, options) do
39
- functions = options[:binding].send(:parse_functions)
64
+ parse_pipeline = ->(input, opts) do
65
+ functions = opts[:binding].send(:parse_functions)
40
66
  pipeline = Representable::Pipeline[*functions] # Pipeline[StopOnExcluded, AssignName, ReadFragment, StopOnNotFound, OverwriteOnNil, Collect[#<Representable::Function::CreateObject:0xa6148ec>, #<Representable::Function::Decorate:0xa6148b0>, Deserialize], Set]
41
67
 
42
68
  pipeline = Representable::Pipeline::Insert.(pipeline, external_populator, replace: Representable::CreateObject::Instance)
@@ -45,12 +71,12 @@ module Reform
45
71
  pipeline = Representable::Pipeline::Insert.(pipeline, Representable::SetValue, delete: true) # FIXME: only diff to options without :populator
46
72
  end
47
73
  else
48
- parse_pipeline = ->(input, options) do
49
- functions = options[:binding].send(:parse_functions)
74
+ parse_pipeline = ->(input, opts) do
75
+ functions = opts[:binding].send(:parse_functions)
50
76
  pipeline = Representable::Pipeline[*functions] # Pipeline[StopOnExcluded, AssignName, ReadFragment, StopOnNotFound, OverwriteOnNil, Collect[#<Representable::Function::CreateObject:0xa6148ec>, #<Representable::Function::Decorate:0xa6148b0>, Deserialize], Set]
51
77
 
52
78
  # FIXME: this won't work with property :name, inherit: true (where there is a populator set already).
53
- pipeline = Representable::Pipeline::Insert.(pipeline, external_populator, replace: Representable::SetValue) if definition[:populator] # FIXME: only diff to options without :populator
79
+ pipeline = Representable::Pipeline::Insert.(pipeline, external_populator, replace: Representable::SetValue) if definition[:populator] # FIXME: only diff to options without :populator
54
80
  pipeline
55
81
  end
56
82
  end
@@ -62,12 +88,12 @@ module Reform
62
88
  deserializer_options.merge!(skip_parse: proc) # TODO: same with skip_parse ==> External
63
89
  end
64
90
 
65
-
66
91
  # per default, everything should be writeable for the deserializer (we're only writing on the form). however, allow turning it off.
67
- deserializer_options.merge!(writeable: true) unless deserializer_options.has_key?(:writeable)
92
+ deserializer_options.merge!(writeable: true) unless deserializer_options.key?(:writeable)
68
93
 
69
94
  definition
70
95
  end
96
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
71
97
  end
72
98
  extend Property
73
99
 
@@ -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,67 +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
- def build_errors
13
- Reform::Contract::Errors.new(self)
14
- end
15
-
16
- module ClassMethods
17
- def validation_group_class
18
- Group
19
- 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
20
14
  end
21
15
 
22
- def self.included(includer)
23
- includer.extend(ClassMethods)
24
- end
25
-
26
- class Group
27
- def initialize
28
- @schemas = []
29
- end
30
-
31
- def instance_exec(&block)
32
- @schemas << block
33
- @validator = Builder.new(@schemas.dup).validation_graph
34
- end
35
-
36
- def call(fields, reform_errors, form)
37
- # a message item looks like: {:confirm_password=>["confirm_password size cannot be less than 2"]}
38
- @validator.with(form: form).call(fields).messages.each do |field, dry_error|
39
- dry_error.each do |attr_error|
40
- reform_errors.add(field, attr_error)
41
- end
42
- end
43
- end
44
-
45
- class Builder < Array
46
- def initialize(array)
47
- super(array)
48
- @validator = Dry::Validation.Form({}, &shift)
49
- end
50
-
51
- def validation_graph
52
- build_graph(@validator)
53
- end
54
-
55
-
56
- private
57
-
58
- def build_graph(validator)
59
- if empty?
60
- return validator
61
- end
62
- build_graph(Dry::Validation.Schema(validator, {}, &shift))
63
- end
64
- end
65
- end
16
+ includer.send :include, validations
17
+ includer.extend validations::ClassMethods
66
18
  end
67
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,46 @@
1
+ ::Dry::Validation.load_extensions(:hints)
2
+
3
+ module Reform::Form::Dry
4
+ module NewApi
5
+
6
+ class Contract < ::Dry::Validation::Contract
7
+ end
8
+
9
+ module Validations
10
+ module ClassMethods
11
+ def validation_group_class
12
+ Group
13
+ end
14
+ end
15
+
16
+ def self.included(includer)
17
+ includer.extend(ClassMethods)
18
+ end
19
+
20
+ class Group
21
+ include InputHash
22
+
23
+ def initialize(options = {})
24
+ @validator = options.fetch(:schema, Contract)
25
+ @schema_inject_params = options.fetch(:with, {})
26
+ end
27
+
28
+ def instance_exec(&block)
29
+ @block = block
30
+ end
31
+
32
+ def call(form)
33
+ # when passing options[:schema] the class instance is already created so we just need to call
34
+ # "call"
35
+ if @validator.is_a?(Class) && @validator <= ::Dry::Validation::Contract
36
+ dynamic_options = {form: form}
37
+ inject_options = @schema_inject_params.merge(dynamic_options)
38
+ @validator = @validator.build(inject_options, &@block)
39
+ end
40
+
41
+ @validator.call(input_hash(form))
42
+ end
43
+ end
44
+ end
45
+ end
46
+ 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
@@ -7,7 +7,7 @@
7
7
  class Reform::Form::Populator
8
8
  def initialize(user_proc)
9
9
  @user_proc = user_proc # the actual `populator: ->{}` block from the user, via ::property.
10
- @value = Declarative::Option(user_proc, instance_exec: true) # we can now process Callable, procs, :symbol.
10
+ @value = Declarative::Option(user_proc, instance_exec: true, callable: Object) # we can now process Callable, procs, :symbol.
11
11
  end
12
12
 
13
13
  def call(input, options)
@@ -26,13 +26,11 @@ 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
- deprecate_positional_args(form, @user_proc, options) do
34
- @value.(form, options)
35
- end
33
+ @value.(form, options) # Declarative::Option call.
36
34
  end
37
35
 
38
36
  def handle_fail(twin, options)
@@ -40,20 +38,9 @@ private
40
38
  end
41
39
 
42
40
  def get(options)
43
- Representable::GetValue.(nil, options)
44
- end
45
-
46
- def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
47
- arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
48
- return yield if arity == 1
49
- 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"
50
- args = []
51
- args << options[:index] if options[:index]
52
- args << options[:representable_options]
53
- form.instance_exec(options[:fragment], options[:model], *args, &proc)
41
+ Representable::GetValue.(nil, options)
54
42
  end
55
43
 
56
-
57
44
  class IfEmpty < self # Populator
58
45
  def call!(options)
59
46
  binding, twin, index, fragment = options[:binding], options[:model], options[:index], options[:fragment] # TODO: remove once we drop 2.0.
@@ -73,7 +60,8 @@ private
73
60
  end
74
61
  end
75
62
 
76
- private
63
+ private
64
+
77
65
  def run!(form, fragment, options)
78
66
  return @user_proc.new if @user_proc.is_a?(Class) # handle populate_if_empty: Class. this excludes using Callables, though.
79
67
 
@@ -89,18 +77,14 @@ private
89
77
 
90
78
  @value.(form, options[:fragment], options[:user_options])
91
79
  end
92
-
93
80
  end
94
81
 
95
82
  # Sync (default) blindly grabs the corresponding form twin and returns it. This might imply that nil is returned,
96
83
  # and in turn #validate! is called on nil.
97
84
  class Sync < self
98
85
  def call!(options)
99
- if options[:binding].array?
100
- return options[:model][options[:index]]
101
- else
102
- options[:model]
103
- end
86
+ return options[:model][options[:index]] if options[:binding].array?
87
+ options[:model]
104
88
  end
105
89
  end
106
90
 
@@ -116,8 +100,8 @@ private
116
100
  # (which population is) to the form.
117
101
  class External
118
102
  def call(input, options)
119
- options[:represented].class.definitions.
120
- get(options[:binding][:name])[:internal_populator].(input, options)
103
+ options[:represented].class.definitions
104
+ .get(options[:binding][:name])[:internal_populator].(input, options)
121
105
  end
122
106
  end
123
107
  end