reform 2.3.0.rc1 → 2.5.0
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/.gitignore +5 -1
- data/.travis.yml +7 -11
- data/CHANGES.md +43 -3
- data/Gemfile +2 -5
- data/ISSUE_TEMPLATE.md +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +7 -9
- data/Rakefile +6 -10
- data/lib/reform/contract.rb +7 -7
- data/lib/reform/contract/custom_error.rb +41 -0
- data/lib/reform/contract/validate.rb +10 -6
- data/lib/reform/errors.rb +27 -15
- data/lib/reform/form.rb +22 -11
- data/lib/reform/form/call.rb +1 -1
- data/lib/reform/form/composition.rb +2 -2
- data/lib/reform/form/dry.rb +22 -60
- data/lib/reform/form/dry/input_hash.rb +37 -0
- data/lib/reform/form/populator.rb +9 -11
- data/lib/reform/form/prepopulate.rb +3 -2
- data/lib/reform/form/validate.rb +19 -12
- data/lib/reform/result.rb +36 -9
- data/lib/reform/validation.rb +10 -8
- data/lib/reform/validation/groups.rb +2 -4
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +9 -9
- data/test/benchmarking.rb +10 -11
- data/test/call_test.rb +8 -8
- data/test/changed_test.rb +13 -13
- data/test/coercion_test.rb +56 -24
- data/test/composition_test.rb +49 -51
- data/test/contract/custom_error_test.rb +55 -0
- data/test/contract_test.rb +18 -18
- data/test/default_test.rb +3 -3
- data/test/deserialize_test.rb +14 -17
- data/test/docs/validation_test.rb +134 -0
- data/test/errors_test.rb +131 -86
- data/test/feature_test.rb +9 -11
- data/test/fixtures/dry_error_messages.yml +65 -52
- data/test/form_option_test.rb +3 -3
- data/test/form_test.rb +6 -6
- data/test/from_test.rb +17 -21
- data/test/inherit_test.rb +28 -35
- data/test/module_test.rb +23 -28
- data/test/parse_option_test.rb +12 -12
- data/test/parse_pipeline_test.rb +3 -3
- data/test/populate_test.rb +146 -93
- data/test/populator_skip_test.rb +3 -4
- data/test/prepopulator_test.rb +20 -21
- data/test/read_only_test.rb +12 -1
- data/test/readable_test.rb +7 -7
- data/test/reform_test.rb +38 -42
- data/test/save_test.rb +16 -19
- data/test/setup_test.rb +15 -15
- data/test/skip_if_test.rb +30 -19
- data/test/skip_setter_and_getter_test.rb +8 -9
- data/test/test_helper.rb +12 -5
- data/test/validate_test.rb +160 -140
- data/test/validation/dry_validation_test.rb +407 -236
- data/test/validation/result_test.rb +29 -31
- data/test/validation_library_provided_test.rb +3 -3
- data/test/virtual_test.rb +46 -6
- data/test/writeable_test.rb +13 -13
- metadata +32 -29
- data/test/readonly_test.rb +0 -14
data/lib/reform/form/call.rb
CHANGED
@@ -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
|
-
[
|
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
|
data/lib/reform/form/dry.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
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
|
@@ -19,76 +25,32 @@ module Reform::Form::Dry
|
|
19
25
|
includer.extend(ClassMethods)
|
20
26
|
end
|
21
27
|
|
22
|
-
|
23
28
|
class Group
|
24
|
-
|
25
|
-
options ||= {}
|
26
|
-
schema_class = options[:schema] || Dry::Validation::Schema
|
27
|
-
@validator = Dry::Validation.Schema(schema_class, build: false)
|
29
|
+
include InputHash
|
28
30
|
|
29
|
-
|
31
|
+
def initialize(**options)
|
32
|
+
@validator = options.fetch(:contract, Contract)
|
33
|
+
@schema_inject_params = options.fetch(:with, {})
|
30
34
|
end
|
31
35
|
|
32
|
-
|
33
|
-
@validator = Dry::Validation.Schema(@validator, build: false, &block)
|
36
|
+
attr_reader :validator, :schema_inject_params, :block
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
@validator.class_eval do
|
38
|
-
configure do
|
39
|
-
keys.each { |k| option k }
|
40
|
-
end
|
41
|
-
end
|
38
|
+
def instance_exec(&block)
|
39
|
+
@block = block
|
42
40
|
end
|
43
41
|
|
44
42
|
def call(form)
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
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
|
73
46
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
symbolize_hash(hash)
|
47
|
+
dynamic_options = { form: form }
|
48
|
+
inject_options = schema_inject_params.merge(dynamic_options)
|
49
|
+
contract.new(inject_options).call(input_hash(form))
|
78
50
|
end
|
79
51
|
|
80
|
-
|
81
|
-
|
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
|
-
}
|
52
|
+
def contract
|
53
|
+
@contract ||= Class.new(validator, &block)
|
92
54
|
end
|
93
55
|
end
|
94
56
|
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
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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]
|
data/lib/reform/form/validate.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
13
|
-
|
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
|
-
|
39
|
-
|
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(
|
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:
|
58
|
+
definitions_from: ->(inline) { inline.definitions },
|
52
59
|
options_from: :deserializer,
|
53
|
-
exclude_options: [
|
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
|
|
data/lib/reform/result.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/reform/validation.rb
CHANGED
@@ -7,7 +7,7 @@ module Reform::Validation
|
|
7
7
|
end
|
8
8
|
|
9
9
|
# DSL.
|
10
|
-
def validation(name=nil, options={}, &block)
|
10
|
+
def validation(name = nil, options = {}, &block)
|
11
11
|
options = deprecate_validation_positional_args(name, options)
|
12
12
|
name = options[:name] # TODO: remove in favor of kw args in 3.0.
|
13
13
|
|
@@ -20,19 +20,17 @@ module Reform::Validation
|
|
20
20
|
def deprecate_validation_positional_args(name, options)
|
21
21
|
if name.is_a?(Symbol)
|
22
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 {
|
23
|
+
return {name: name}.merge(options)
|
24
24
|
end
|
25
25
|
|
26
|
-
if name.nil?
|
27
|
-
return { name: :default }.merge(options)
|
28
|
-
end
|
26
|
+
return {name: :default}.merge(options) if name.nil?
|
29
27
|
|
30
|
-
{
|
28
|
+
{name: :default}.merge(name)
|
31
29
|
end
|
32
30
|
|
33
31
|
def validation_group_class
|
34
|
-
raise NoValidationLibraryError,
|
35
|
-
|
32
|
+
raise NoValidationLibraryError, "no validation library loaded. Please include a " +
|
33
|
+
"validation library such as Reform::Form::Dry"
|
36
34
|
end
|
37
35
|
end
|
38
36
|
|
@@ -40,6 +38,10 @@ module Reform::Validation
|
|
40
38
|
includer.extend(ClassMethods)
|
41
39
|
end
|
42
40
|
|
41
|
+
def valid?
|
42
|
+
validate({})
|
43
|
+
end
|
44
|
+
|
43
45
|
NoValidationLibraryError = Class.new(RuntimeError)
|
44
46
|
end
|
45
47
|
|
@@ -23,7 +23,7 @@ module Reform::Validation
|
|
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]
|
@@ -36,7 +36,6 @@ module Reform::Validation
|
|
36
36
|
cfg[1]
|
37
37
|
end
|
38
38
|
|
39
|
-
|
40
39
|
# Runs all validations groups according to their rules and returns all Result objects.
|
41
40
|
class Validate
|
42
41
|
def self.call(groups, form)
|
@@ -44,14 +43,13 @@ module Reform::Validation
|
|
44
43
|
|
45
44
|
groups.collect do |(name, group, options)|
|
46
45
|
next unless evaluate?(options[:if], results, form)
|
47
|
-
|
48
46
|
results[name] = group.(form) # run validation for group. store and collect <Result>.
|
49
47
|
end
|
50
48
|
end
|
51
49
|
|
52
50
|
def self.evaluate?(depends_on, results, form)
|
53
51
|
return true if depends_on.nil?
|
54
|
-
return results[depends_on].success? if depends_on.is_a?(Symbol)
|
52
|
+
return !results[depends_on].nil? && results[depends_on].success? if depends_on.is_a?(Symbol)
|
55
53
|
form.instance_exec(results, &depends_on)
|
56
54
|
end
|
57
55
|
end
|