reform 2.2.4 → 2.6.2
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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +17 -0
- data/.gitignore +5 -1
- data/CHANGES.md +80 -4
- data/CONTRIBUTING.md +31 -0
- data/Gemfile +2 -16
- data/ISSUE_TEMPLATE.md +25 -0
- data/LICENSE.txt +1 -1
- data/README.md +10 -12
- data/Rakefile +6 -10
- data/lib/reform/contract/custom_error.rb +41 -0
- data/lib/reform/contract/validate.rb +53 -23
- data/lib/reform/contract.rb +7 -17
- data/lib/reform/errors.rb +61 -0
- data/lib/reform/form/call.rb +1 -1
- data/lib/reform/form/composition.rb +2 -2
- data/lib/reform/form/dry/input_hash.rb +37 -0
- data/lib/reform/form/dry.rb +25 -35
- data/lib/reform/form/populator.rb +20 -26
- data/lib/reform/form/prepopulate.rb +5 -4
- data/lib/reform/form/validate.rb +29 -14
- data/lib/reform/form.rb +36 -10
- data/lib/reform/result.rb +90 -0
- data/lib/reform/validation/groups.rb +12 -28
- data/lib/reform/validation.rb +19 -11
- data/lib/reform/version.rb +1 -1
- data/lib/reform.rb +1 -0
- data/reform.gemspec +13 -13
- data/test/benchmarking.rb +39 -6
- data/test/call_test.rb +9 -9
- data/test/changed_test.rb +14 -14
- data/test/coercion_test.rb +57 -25
- data/test/composition_test.rb +85 -49
- data/test/contract/custom_error_test.rb +55 -0
- data/test/contract_test.rb +20 -20
- data/test/default_test.rb +4 -4
- data/test/deserialize_test.rb +17 -20
- data/test/docs/validation_test.rb +134 -0
- data/test/errors_test.rb +142 -82
- data/test/feature_test.rb +10 -12
- data/test/fixtures/dry_error_messages.yml +83 -23
- data/test/form_option_test.rb +5 -5
- data/test/form_test.rb +8 -8
- data/test/from_test.rb +18 -22
- data/test/inherit_test.rb +54 -68
- data/test/module_test.rb +27 -32
- data/test/parse_option_test.rb +40 -0
- data/test/parse_pipeline_test.rb +4 -4
- data/test/populate_test.rb +217 -100
- data/test/populator_skip_test.rb +11 -11
- data/test/prepopulator_test.rb +24 -25
- data/test/read_only_test.rb +12 -1
- data/test/readable_test.rb +9 -9
- data/test/reform_test.rb +47 -66
- data/test/save_test.rb +35 -23
- data/test/setup_test.rb +17 -17
- data/test/skip_if_test.rb +33 -22
- data/test/skip_setter_and_getter_test.rb +9 -10
- data/test/test_helper.rb +23 -14
- data/test/validate_test.rb +165 -145
- data/test/validation/dry_validation_test.rb +630 -146
- data/test/validation/result_test.rb +77 -0
- data/test/validation_library_provided_test.rb +16 -0
- data/test/virtual_test.rb +47 -7
- data/test/writeable_test.rb +38 -9
- metadata +46 -38
- data/.travis.yml +0 -11
- data/gemfiles/Gemfile.disposable-0.3 +0 -6
- data/lib/reform/contract/errors.rb +0 -43
- data/lib/reform/form/mongoid.rb +0 -37
- data/lib/reform/form/orm.rb +0 -26
- data/lib/reform/mongoid.rb +0 -4
- data/test/deprecation_test.rb +0 -27
- data/test/readonly_test.rb +0 -14
- data/test/validation/dry_test.rb +0 -60
- data/test/validation/errors.yml +0 -4
data/lib/reform/form/dry.rb
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
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
|
|
9
15
|
end
|
|
10
16
|
|
|
11
17
|
module Validations
|
|
12
|
-
def build_errors
|
|
13
|
-
Reform::Contract::Errors.new(self)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
18
|
module ClassMethods
|
|
17
19
|
def validation_group_class
|
|
18
20
|
Group
|
|
@@ -24,43 +26,31 @@ module Reform::Form::Dry
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
class Group
|
|
27
|
-
|
|
28
|
-
@schemas = []
|
|
29
|
-
end
|
|
29
|
+
include InputHash
|
|
30
30
|
|
|
31
|
-
def
|
|
32
|
-
@
|
|
33
|
-
@
|
|
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
|
|
31
|
+
def initialize(options)
|
|
32
|
+
@validator = options.fetch(:contract, Contract)
|
|
33
|
+
@schema_inject_params = options.fetch(:with, {})
|
|
43
34
|
end
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
def initialize(array)
|
|
47
|
-
super(array)
|
|
48
|
-
@validator = Dry::Validation.Form({}, &shift)
|
|
49
|
-
end
|
|
36
|
+
attr_reader :validator, :schema_inject_params, :block
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
def instance_exec(&block)
|
|
39
|
+
@block = block
|
|
40
|
+
end
|
|
54
41
|
|
|
42
|
+
def call(form)
|
|
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
|
|
55
46
|
|
|
56
|
-
|
|
47
|
+
dynamic_options = { form: form }
|
|
48
|
+
inject_options = schema_inject_params.merge(dynamic_options)
|
|
49
|
+
contract.new(**inject_options).call(input_hash(form))
|
|
50
|
+
end
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return validator
|
|
61
|
-
end
|
|
62
|
-
build_graph(Dry::Validation.Schema(validator, {}, &shift))
|
|
63
|
-
end
|
|
52
|
+
def contract
|
|
53
|
+
@contract ||= Class.new(validator, &block)
|
|
64
54
|
end
|
|
65
55
|
end
|
|
66
56
|
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 =
|
|
10
|
+
@value = ::Representable::Option(user_proc) # we can now process Callable, procs, :symbol.
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def call(input, options)
|
|
@@ -26,13 +26,21 @@ 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]
|
|
33
|
+
evaluate_option(form, options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def evaluate_option(form, options)
|
|
37
|
+
if @user_proc.is_a?(Uber::Callable) && @user_proc.method(:call).arity == 2 # def call(form, options)
|
|
38
|
+
warn %{[Reform] Accepting `form` as a positional argument in `:populator` will be deprecated. Please use `def call(form:, **options)` signature instead.}
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
@value.(form, options)
|
|
40
|
+
return @value.(form, exec_context: form, keyword_arguments: options)
|
|
35
41
|
end
|
|
42
|
+
|
|
43
|
+
@value.(exec_context: form, keyword_arguments: options.merge(form: form)) # Representable::Option call
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
def handle_fail(twin, options)
|
|
@@ -40,20 +48,9 @@ private
|
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
def get(options)
|
|
43
|
-
|
|
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)
|
|
51
|
+
Representable::GetValue.(nil, options)
|
|
54
52
|
end
|
|
55
53
|
|
|
56
|
-
|
|
57
54
|
class IfEmpty < self # Populator
|
|
58
55
|
def call!(options)
|
|
59
56
|
binding, twin, index, fragment = options[:binding], options[:model], options[:index], options[:fragment] # TODO: remove once we drop 2.0.
|
|
@@ -73,12 +70,13 @@ private
|
|
|
73
70
|
end
|
|
74
71
|
end
|
|
75
72
|
|
|
76
|
-
|
|
73
|
+
private
|
|
74
|
+
|
|
77
75
|
def run!(form, fragment, options)
|
|
78
76
|
return @user_proc.new if @user_proc.is_a?(Class) # handle populate_if_empty: Class. this excludes using Callables, though.
|
|
79
77
|
|
|
80
78
|
deprecate_positional_args(form, @user_proc, options) do
|
|
81
|
-
|
|
79
|
+
evaluate_option(form, options)
|
|
82
80
|
end
|
|
83
81
|
end
|
|
84
82
|
|
|
@@ -89,18 +87,14 @@ private
|
|
|
89
87
|
|
|
90
88
|
@value.(form, options[:fragment], options[:user_options])
|
|
91
89
|
end
|
|
92
|
-
|
|
93
90
|
end
|
|
94
91
|
|
|
95
92
|
# Sync (default) blindly grabs the corresponding form twin and returns it. This might imply that nil is returned,
|
|
96
93
|
# and in turn #validate! is called on nil.
|
|
97
94
|
class Sync < self
|
|
98
95
|
def call!(options)
|
|
99
|
-
if options[:binding].array?
|
|
100
|
-
|
|
101
|
-
else
|
|
102
|
-
options[:model]
|
|
103
|
-
end
|
|
96
|
+
return options[:model][options[:index]] if options[:binding].array?
|
|
97
|
+
options[:model]
|
|
104
98
|
end
|
|
105
99
|
end
|
|
106
100
|
|
|
@@ -116,8 +110,8 @@ private
|
|
|
116
110
|
# (which population is) to the form.
|
|
117
111
|
class External
|
|
118
112
|
def call(input, options)
|
|
119
|
-
options[:represented].class.definitions
|
|
120
|
-
|
|
113
|
+
options[:represented].class.definitions
|
|
114
|
+
.get(options[:binding][:name])[:internal_populator].(input, options)
|
|
121
115
|
end
|
|
122
116
|
end
|
|
123
117
|
end
|
|
@@ -1,18 +1,19 @@
|
|
|
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]
|
|
15
|
-
|
|
16
|
+
::Representable::Option(block).(exec_context: self, keyword_arguments: options)
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -21,4 +22,4 @@ private
|
|
|
21
22
|
Disposable::Twin::PropertyProcessor.new(dfn, self).() { |form| form.prepopulate!(options) }
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
|
-
end
|
|
25
|
+
end
|
data/lib/reform/form/validate.rb
CHANGED
|
@@ -4,57 +4,72 @@ module Reform::Form::Validate
|
|
|
4
4
|
class AllBlank
|
|
5
5
|
include Uber::Callable
|
|
6
6
|
|
|
7
|
-
def call(
|
|
8
|
-
params = options[:input]
|
|
7
|
+
def call(input:, binding:, **)
|
|
9
8
|
# TODO: Schema should provide property names as plain list.
|
|
10
|
-
|
|
9
|
+
# ensure param keys are strings.
|
|
10
|
+
params = input.each_with_object({}) { |(k, v), hash|
|
|
11
|
+
hash[k.to_s] = v
|
|
12
|
+
}
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
# return false if any property inputs are populated.
|
|
15
|
+
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.
|
|
27
|
+
@input_params = params # we want to store these for access via dry later
|
|
21
28
|
block_given? ? yield(params) : deserialize(params)
|
|
22
29
|
|
|
23
30
|
super() # run the actual validation on self.
|
|
24
31
|
end
|
|
32
|
+
attr_reader :input_params # make the raw input params public
|
|
25
33
|
|
|
26
34
|
def deserialize(params)
|
|
27
35
|
params = deserialize!(params)
|
|
28
36
|
deserializer.new(self).from_hash(params)
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
private
|
|
39
|
+
private
|
|
40
|
+
|
|
32
41
|
# Meant to return params processable by the representer. This is the hook for munching date fields, etc.
|
|
33
42
|
def deserialize!(params)
|
|
34
43
|
# NOTE: it is completely up to the form user how they want to deserialize (e.g. using an external JSON-API representer).
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
# use the deserializer as an external instance to operate on the Twin API,
|
|
45
|
+
# e.g. adding new items in collections using #<< etc.
|
|
37
46
|
# DISCUSS: using self here will call the form's setters like title= which might be overridden.
|
|
38
47
|
params
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
# Default deserializer for hash.
|
|
42
51
|
# This is input-specific, e.g. Hash, JSON, or XML.
|
|
43
|
-
def deserializer(source=self.class, options={}) # called on top-level, only, for now.
|
|
44
|
-
deserializer = Disposable::Rescheme.from(
|
|
52
|
+
def deserializer!(source = self.class, options = {}) # called on top-level, only, for now.
|
|
53
|
+
deserializer = Disposable::Rescheme.from(
|
|
54
|
+
source,
|
|
45
55
|
{
|
|
46
56
|
include: [Representable::Hash::AllowSymbols, Representable::Hash],
|
|
47
57
|
superclass: Representable::Decorator,
|
|
48
|
-
definitions_from:
|
|
58
|
+
definitions_from: ->(inline) { inline.definitions },
|
|
49
59
|
options_from: :deserializer,
|
|
50
|
-
exclude_options: [
|
|
60
|
+
exclude_options: %i[default populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
|
|
51
61
|
}.merge(options)
|
|
52
62
|
)
|
|
53
63
|
|
|
54
64
|
deserializer
|
|
55
65
|
end
|
|
56
66
|
|
|
67
|
+
def deserializer(*args)
|
|
68
|
+
# DISCUSS: should we simply delegate to class and sort out memoizing there?
|
|
69
|
+
self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
|
|
70
|
+
end
|
|
57
71
|
|
|
58
|
-
|
|
72
|
+
def self.included(includer)
|
|
73
|
+
includer.singleton_class.send :attr_accessor, :deserializer_class
|
|
59
74
|
end
|
|
60
75
|
end
|
data/lib/reform/form.rb
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
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,
|
|
62
|
+
# first, letrepresentable compute the pipeline functions by invoking #parse_functions.
|
|
37
63
|
if definition[:nested]
|
|
38
|
-
parse_pipeline = ->(input,
|
|
39
|
-
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,
|
|
49
|
-
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,
|
|
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.
|
|
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
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Reform
|
|
2
|
+
class Contract < Disposable::Twin
|
|
3
|
+
# Collects all native results of a form of all groups and provides
|
|
4
|
+
# a unified API: #success?, #errors, #messages, #hints.
|
|
5
|
+
# #success? returns validity of the branch.
|
|
6
|
+
class Result
|
|
7
|
+
def initialize(results, nested_results = []) # DISCUSS: do we like this?
|
|
8
|
+
@results = results # native Result objects, e.g. `#<Dry::Validation::Result output={:title=>"Fallout", :composer=>nil} errors={}>`
|
|
9
|
+
@failure = (results + nested_results).find(&:failure?) # TODO: test nested.
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def failure?; @failure end
|
|
13
|
+
|
|
14
|
+
def success?; !failure? end
|
|
15
|
+
|
|
16
|
+
def errors(*args); filter_for(:errors, *args) end
|
|
17
|
+
|
|
18
|
+
def messages(*args); filter_for(:messages, *args) end
|
|
19
|
+
|
|
20
|
+
def hints(*args); filter_for(:hints, *args) end
|
|
21
|
+
|
|
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. )
|
|
33
|
+
def filter_for(method, *args)
|
|
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
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Note: this class will be redundant in Reform 3, where the public API
|
|
46
|
+
# allows/enforces to pass options to #errors (e.g. errors(locale: "br"))
|
|
47
|
+
# which means we don't have to "lazy-handle" that with "pointers".
|
|
48
|
+
# :private:
|
|
49
|
+
class Pointer
|
|
50
|
+
extend Forwardable
|
|
51
|
+
|
|
52
|
+
def initialize(result, path)
|
|
53
|
+
@result, @path = result, path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def_delegators :@result, :success?, :failure?
|
|
57
|
+
|
|
58
|
+
def errors(*args); traverse_for(:errors, *args) end
|
|
59
|
+
|
|
60
|
+
def messages(*args); traverse_for(:messages, *args) end
|
|
61
|
+
|
|
62
|
+
def hints(*args); traverse_for(:hints, *args) end
|
|
63
|
+
|
|
64
|
+
def advance(*path)
|
|
65
|
+
path = @path + path.compact # remove index if nil.
|
|
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)
|
|
74
|
+
|
|
75
|
+
Pointer.new(@result, path)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def traverse(hash, path)
|
|
81
|
+
path.inject(hash) { |errs, segment| errs[segment] || {} } # FIXME. test if all segments present.
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def traverse_for(method, *args)
|
|
85
|
+
traverse(@result.public_send(method, *args), @path) # TODO: return [] if nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -19,11 +19,11 @@ module Reform::Validation
|
|
|
19
19
|
|
|
20
20
|
i = index_for(options)
|
|
21
21
|
|
|
22
|
-
self.insert(i, [name, group = @group_class.new, options]) # Group.new
|
|
22
|
+
self.insert(i, [name, group = @group_class.new(options), options]) # Group.new
|
|
23
23
|
group
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
private
|
|
27
27
|
|
|
28
28
|
def index_for(options)
|
|
29
29
|
return find_index { |el| el.first == options[:after] } + 1 if options[:after]
|
|
@@ -31,43 +31,27 @@ module Reform::Validation
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def [](name)
|
|
34
|
-
cfg = find { |
|
|
34
|
+
cfg = find { |c| c.first == name }
|
|
35
35
|
return unless cfg
|
|
36
36
|
cfg[1]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class Result # DISCUSS: could be in Groups.
|
|
43
|
-
def initialize(groups)
|
|
44
|
-
@groups = groups
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def call(fields, errors, form)
|
|
48
|
-
result = true
|
|
39
|
+
# Runs all validations groups according to their rules and returns all Result objects.
|
|
40
|
+
class Validate
|
|
41
|
+
def self.call(groups, form)
|
|
49
42
|
results = {}
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if evaluate_if(depends_on, results, form)
|
|
56
|
-
# puts "evaluating #{group.instance_variable_get(:@validator).instance_variable_get(:@checker).inspect}"
|
|
57
|
-
results[name] = group.(fields, errors, form).empty? # validate.
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
result &= errors.empty?
|
|
44
|
+
groups.collect do |(name, group, options)|
|
|
45
|
+
next unless evaluate?(options[:if], results, form)
|
|
46
|
+
results[name] = group.(form) # run validation for group. store and collect <Result>.
|
|
61
47
|
end
|
|
62
|
-
|
|
63
|
-
result
|
|
64
48
|
end
|
|
65
49
|
|
|
66
|
-
def
|
|
50
|
+
def self.evaluate?(depends_on, results, form)
|
|
67
51
|
return true if depends_on.nil?
|
|
68
|
-
return results[depends_on] if depends_on.is_a?(Symbol)
|
|
52
|
+
return !results[depends_on].nil? && results[depends_on].success? if depends_on.is_a?(Symbol)
|
|
69
53
|
form.instance_exec(results, &depends_on)
|
|
70
54
|
end
|
|
71
55
|
end
|
|
72
56
|
end
|
|
73
|
-
end
|
|
57
|
+
end
|
data/lib/reform/validation.rb
CHANGED
|
@@ -3,28 +3,34 @@
|
|
|
3
3
|
module Reform::Validation
|
|
4
4
|
module ClassMethods
|
|
5
5
|
def validation_groups
|
|
6
|
-
@groups ||= Groups.new(validation_group_class)
|
|
6
|
+
@groups ||= Groups.new(validation_group_class)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
# DSL.
|
|
10
|
-
def validation(name
|
|
11
|
-
|
|
10
|
+
def validation(name = nil, options = {}, &block)
|
|
11
|
+
options = deprecate_validation_positional_args(name, options)
|
|
12
|
+
name = options[:name] # TODO: remove in favor of kw args in 3.0.
|
|
12
13
|
|
|
14
|
+
heritage.record(:validation, options, &block)
|
|
13
15
|
group = validation_groups.add(name, options)
|
|
14
16
|
|
|
15
17
|
group.instance_exec(&block)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
def deprecate_validation_positional_args(name, options)
|
|
21
|
+
if name.is_a?(Symbol)
|
|
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)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return {name: :default}.merge(options) if name.nil?
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
validation(:default, inherit: true) { validate *args, &block }
|
|
28
|
+
{name: :default}.merge(name)
|
|
24
29
|
end
|
|
25
30
|
|
|
26
|
-
def
|
|
27
|
-
|
|
31
|
+
def validation_group_class
|
|
32
|
+
raise NoValidationLibraryError, "no validation library loaded. Please include a " +
|
|
33
|
+
"validation library such as Reform::Form::Dry"
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
|
|
@@ -33,8 +39,10 @@ module Reform::Validation
|
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
def valid?
|
|
36
|
-
|
|
42
|
+
validate({})
|
|
37
43
|
end
|
|
44
|
+
|
|
45
|
+
NoValidationLibraryError = Class.new(RuntimeError)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
require "reform/validation/groups"
|
data/lib/reform/version.rb
CHANGED
data/lib/reform.rb
CHANGED
data/reform.gemspec
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
lib = File.expand_path(
|
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
-
require
|
|
3
|
+
require "reform/version"
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "reform"
|
|
7
7
|
spec.version = Reform::VERSION
|
|
8
|
-
spec.authors = ["Nick Sutterer", "
|
|
9
|
-
spec.email = ["apotonick@gmail.com", "
|
|
10
|
-
spec.description =
|
|
11
|
-
spec.summary =
|
|
12
|
-
spec.homepage = "https://github.com/
|
|
8
|
+
spec.authors = ["Nick Sutterer", "Fran Worley"]
|
|
9
|
+
spec.email = ["apotonick@gmail.com", "frances@safetytoolbox.co.uk"]
|
|
10
|
+
spec.description = "Form object decoupled from models."
|
|
11
|
+
spec.summary = "Form object decoupled from models with validation, population and presentation."
|
|
12
|
+
spec.homepage = "https://github.com/trailblazer/reform"
|
|
13
13
|
spec.license = "MIT"
|
|
14
14
|
|
|
15
15
|
spec.files = `git ls-files`.split($/)
|
|
16
|
-
spec.executables = spec.files.grep(%r
|
|
16
|
+
spec.executables = spec.files.grep(%r(^bin/)) { |f| File.basename(f) }
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
18
18
|
spec.require_paths = ["lib"]
|
|
19
19
|
|
|
20
|
-
spec.add_dependency "disposable",
|
|
21
|
-
spec.add_dependency "representable",
|
|
20
|
+
spec.add_dependency "disposable", ">= 0.5.0", "< 1.0.0"
|
|
21
|
+
spec.add_dependency "representable", ">= 3.1.1", "< 4"
|
|
22
|
+
spec.add_dependency "uber", "< 0.2.0"
|
|
22
23
|
|
|
23
24
|
spec.add_development_dependency "bundler"
|
|
24
|
-
spec.add_development_dependency "rake"
|
|
25
25
|
spec.add_development_dependency "minitest"
|
|
26
|
-
spec.add_development_dependency "
|
|
26
|
+
spec.add_development_dependency "minitest-line"
|
|
27
27
|
spec.add_development_dependency "multi_json"
|
|
28
|
-
spec.add_development_dependency "
|
|
28
|
+
spec.add_development_dependency "rake"
|
|
29
29
|
end
|