reform 2.2.4 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
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