reform 2.2.4
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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +11 -0
- data/CHANGES.md +415 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +339 -0
- data/Rakefile +15 -0
- data/TODO.md +45 -0
- data/gemfiles/Gemfile.disposable-0.3 +6 -0
- data/lib/reform.rb +8 -0
- data/lib/reform/contract.rb +77 -0
- data/lib/reform/contract/errors.rb +43 -0
- data/lib/reform/contract/validate.rb +33 -0
- data/lib/reform/form.rb +94 -0
- data/lib/reform/form/call.rb +23 -0
- data/lib/reform/form/coercion.rb +3 -0
- data/lib/reform/form/composition.rb +34 -0
- data/lib/reform/form/dry.rb +67 -0
- data/lib/reform/form/module.rb +27 -0
- data/lib/reform/form/mongoid.rb +37 -0
- data/lib/reform/form/orm.rb +26 -0
- data/lib/reform/form/populator.rb +123 -0
- data/lib/reform/form/prepopulate.rb +24 -0
- data/lib/reform/form/validate.rb +60 -0
- data/lib/reform/mongoid.rb +4 -0
- data/lib/reform/validation.rb +40 -0
- data/lib/reform/validation/groups.rb +73 -0
- data/lib/reform/version.rb +3 -0
- data/reform.gemspec +29 -0
- data/test/benchmarking.rb +26 -0
- data/test/call_test.rb +23 -0
- data/test/changed_test.rb +41 -0
- data/test/coercion_test.rb +66 -0
- data/test/composition_test.rb +149 -0
- data/test/contract_test.rb +77 -0
- data/test/default_test.rb +22 -0
- data/test/deprecation_test.rb +27 -0
- data/test/deserialize_test.rb +104 -0
- data/test/errors_test.rb +165 -0
- data/test/feature_test.rb +65 -0
- data/test/fixtures/dry_error_messages.yml +44 -0
- data/test/form_option_test.rb +24 -0
- data/test/form_test.rb +57 -0
- data/test/from_test.rb +75 -0
- data/test/inherit_test.rb +119 -0
- data/test/module_test.rb +142 -0
- data/test/parse_pipeline_test.rb +15 -0
- data/test/populate_test.rb +270 -0
- data/test/populator_skip_test.rb +28 -0
- data/test/prepopulator_test.rb +112 -0
- data/test/read_only_test.rb +3 -0
- data/test/readable_test.rb +30 -0
- data/test/readonly_test.rb +14 -0
- data/test/reform_test.rb +223 -0
- data/test/save_test.rb +89 -0
- data/test/setup_test.rb +48 -0
- data/test/skip_if_test.rb +74 -0
- data/test/skip_setter_and_getter_test.rb +54 -0
- data/test/test_helper.rb +49 -0
- data/test/validate_test.rb +420 -0
- data/test/validation/dry_test.rb +60 -0
- data/test/validation/dry_validation_test.rb +352 -0
- data/test/validation/errors.yml +4 -0
- data/test/virtual_test.rb +24 -0
- data/test/writeable_test.rb +29 -0
- metadata +265 -0
data/lib/reform.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require "uber/inheritable_attr"
|
2
|
+
|
3
|
+
module Reform
|
4
|
+
# Define your form structure and its validations. Instantiate it with a model,
|
5
|
+
# and then +validate+ this object graph.
|
6
|
+
class Contract < Disposable::Twin
|
7
|
+
require "disposable/twin/composition" # Expose.
|
8
|
+
include Expose
|
9
|
+
|
10
|
+
feature Setup
|
11
|
+
feature Setup::SkipSetter
|
12
|
+
feature Default
|
13
|
+
|
14
|
+
def self.default_nested_class
|
15
|
+
Contract
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.property(name, options={}, &block)
|
19
|
+
if twin = options.delete(:form)
|
20
|
+
options[:twin] = twin
|
21
|
+
end
|
22
|
+
|
23
|
+
if validates_options = options[:validates]
|
24
|
+
validates name, validates_options
|
25
|
+
end
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
# FIXME: test me.
|
31
|
+
def self.properties(*args)
|
32
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
33
|
+
args.each { |name| property(name, options.dup) }
|
34
|
+
end
|
35
|
+
|
36
|
+
require "reform/contract/errors"
|
37
|
+
require 'reform/contract/validate'
|
38
|
+
include Reform::Contract::Validate
|
39
|
+
|
40
|
+
require "reform/validation"
|
41
|
+
include Reform::Validation # ::validates and #valid?
|
42
|
+
|
43
|
+
# FIXME: this is only for #to_nested_hash, #sync shouldn't be part of Contract.
|
44
|
+
require "disposable/twin/sync"
|
45
|
+
include Disposable::Twin::Sync
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
# module ValidatesWarning
|
50
|
+
# def validates(*)
|
51
|
+
# raise "[Reform] Please include either Reform::Form::ActiveModel::Validations or Reform::Form::Lotus in your form class."
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
# extend ValidatesWarning
|
55
|
+
|
56
|
+
private
|
57
|
+
# DISCUSS: separate file?
|
58
|
+
module Readonly
|
59
|
+
def readonly?(name)
|
60
|
+
options_for(name)[:writeable] == false
|
61
|
+
end
|
62
|
+
def options_for(name)
|
63
|
+
self.class.options_for(name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.options_for(name)
|
68
|
+
definitions.get(name)
|
69
|
+
end
|
70
|
+
include Readonly
|
71
|
+
|
72
|
+
|
73
|
+
def self.clone # TODO: test. THIS IS ONLY FOR Trailblazer when contract gets cloned in suboperation.
|
74
|
+
Class.new(self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Reform::Contract::Errors
|
2
|
+
def initialize(*)
|
3
|
+
@errors = {}
|
4
|
+
end
|
5
|
+
|
6
|
+
module Merge
|
7
|
+
def merge!(errors, prefix)
|
8
|
+
errors.messages.each do |field, msgs|
|
9
|
+
unless field.to_sym == :base
|
10
|
+
field = (prefix+[field]).join(".").to_sym # TODO: why is that a symbol in Rails?
|
11
|
+
end
|
12
|
+
|
13
|
+
msgs.each do |msg|
|
14
|
+
next if messages[field] and messages[field].include?(msg)
|
15
|
+
add(field, msg)
|
16
|
+
end # Forms now contains a plain errors hash. the errors for each item are still available in item.errors.
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
messages.inspect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
include Merge
|
25
|
+
|
26
|
+
def add(field, message)
|
27
|
+
@errors[field] ||= []
|
28
|
+
@errors[field] << message
|
29
|
+
end
|
30
|
+
|
31
|
+
def messages
|
32
|
+
@errors
|
33
|
+
end
|
34
|
+
|
35
|
+
def empty?
|
36
|
+
@errors.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
# needed by Rails form builder.
|
40
|
+
def [](name)
|
41
|
+
@errors[name] || []
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Reform::Contract::Validate
|
2
|
+
def initialize(*)
|
3
|
+
super
|
4
|
+
@errors = build_errors
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
def validate
|
10
|
+
validate!(errors, [])
|
11
|
+
|
12
|
+
errors.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!(errors, prefix)
|
16
|
+
validate_nested!(nested_errors = build_errors, prefix) # call valid? recursively and collect nested errors.
|
17
|
+
|
18
|
+
valid? # calls AM/Lotus validators and invokes self.errors=.
|
19
|
+
|
20
|
+
errors.merge!(self.errors, prefix) # local errors.
|
21
|
+
errors.merge!(nested_errors, [])
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# runs form.validate! on all nested forms
|
27
|
+
def validate_nested!(errors, prefixes)
|
28
|
+
schema.each(twin: true) do |dfn|
|
29
|
+
# recursively call valid? on nested form.
|
30
|
+
Disposable::Twin::PropertyProcessor.new(dfn, self).() { |form| form.validate!(errors, prefixes+[dfn[:name]]) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/reform/form.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
module Reform
|
2
|
+
class Form < Contract
|
3
|
+
def self.default_nested_class
|
4
|
+
Form
|
5
|
+
end
|
6
|
+
|
7
|
+
require "reform/form/validate"
|
8
|
+
include Validate # override Contract#validate with additional behaviour.
|
9
|
+
|
10
|
+
require "reform/form/populator"
|
11
|
+
|
12
|
+
# called after populator: form.deserialize(params)
|
13
|
+
# as this only included in the typed pipeline, it's not applied for scalars.
|
14
|
+
Deserialize = ->(input, options) { input.deserialize(options[:fragment]) } # TODO: (result:, fragment:, **o) once we drop 2.0.
|
15
|
+
|
16
|
+
module Property
|
17
|
+
# 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
|
+
definition.merge!(deserializer: {}) unless definition[:deserializer] # always keep :deserializer per property.
|
21
|
+
|
22
|
+
deserializer_options = definition[:deserializer]
|
23
|
+
|
24
|
+
# Populators
|
25
|
+
internal_populator = Populator::Sync.new(nil)
|
26
|
+
if block = definition[:populate_if_empty]
|
27
|
+
internal_populator = Populator::IfEmpty.new(block)
|
28
|
+
end
|
29
|
+
if block = definition[:populator] # populator wins over populate_if_empty when :inherit
|
30
|
+
internal_populator = Populator.new(block)
|
31
|
+
end
|
32
|
+
definition.merge!(internal_populator: internal_populator) unless options[:internal_populator]
|
33
|
+
external_populator = Populator::External.new
|
34
|
+
|
35
|
+
# 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.
|
37
|
+
if definition[:nested]
|
38
|
+
parse_pipeline = ->(input, options) do
|
39
|
+
functions = options[:binding].send(:parse_functions)
|
40
|
+
pipeline = Representable::Pipeline[*functions] # Pipeline[StopOnExcluded, AssignName, ReadFragment, StopOnNotFound, OverwriteOnNil, Collect[#<Representable::Function::CreateObject:0xa6148ec>, #<Representable::Function::Decorate:0xa6148b0>, Deserialize], Set]
|
41
|
+
|
42
|
+
pipeline = Representable::Pipeline::Insert.(pipeline, external_populator, replace: Representable::CreateObject::Instance)
|
43
|
+
pipeline = Representable::Pipeline::Insert.(pipeline, Representable::Decorate, delete: true)
|
44
|
+
pipeline = Representable::Pipeline::Insert.(pipeline, Deserialize, replace: Representable::Deserialize)
|
45
|
+
pipeline = Representable::Pipeline::Insert.(pipeline, Representable::SetValue, delete: true) # FIXME: only diff to options without :populator
|
46
|
+
end
|
47
|
+
else
|
48
|
+
parse_pipeline = ->(input, options) do
|
49
|
+
functions = options[:binding].send(:parse_functions)
|
50
|
+
pipeline = Representable::Pipeline[*functions] # Pipeline[StopOnExcluded, AssignName, ReadFragment, StopOnNotFound, OverwriteOnNil, Collect[#<Representable::Function::CreateObject:0xa6148ec>, #<Representable::Function::Decorate:0xa6148b0>, Deserialize], Set]
|
51
|
+
|
52
|
+
# 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
|
54
|
+
pipeline
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
deserializer_options[:parse_pipeline] ||= parse_pipeline
|
59
|
+
|
60
|
+
if proc = definition[:skip_if]
|
61
|
+
proc = Reform::Form::Validate::Skip::AllBlank.new if proc == :all_blank
|
62
|
+
deserializer_options.merge!(skip_parse: proc) # TODO: same with skip_parse ==> External
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# 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)
|
68
|
+
|
69
|
+
definition
|
70
|
+
end
|
71
|
+
end
|
72
|
+
extend Property
|
73
|
+
|
74
|
+
require "disposable/twin/changed"
|
75
|
+
feature Disposable::Twin::Changed
|
76
|
+
|
77
|
+
require "disposable/twin/sync"
|
78
|
+
feature Disposable::Twin::Sync
|
79
|
+
feature Disposable::Twin::Sync::SkipGetter
|
80
|
+
|
81
|
+
require "disposable/twin/save"
|
82
|
+
feature Disposable::Twin::Save
|
83
|
+
|
84
|
+
require "reform/form/prepopulate"
|
85
|
+
include Prepopulate
|
86
|
+
|
87
|
+
def skip!
|
88
|
+
Representable::Pipeline::Stop
|
89
|
+
end
|
90
|
+
|
91
|
+
require "reform/form/call"
|
92
|
+
include Call
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Reform::Form::Call
|
2
|
+
def call(params, &block)
|
3
|
+
bool = validate(params, &block)
|
4
|
+
|
5
|
+
Result.new(bool, self)
|
6
|
+
end
|
7
|
+
|
8
|
+
# TODO: the result object might soon come from dry.
|
9
|
+
class Result < SimpleDelegator
|
10
|
+
def initialize(success, data)
|
11
|
+
@success = success
|
12
|
+
super(data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def success?
|
16
|
+
@success
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure?
|
20
|
+
! @success
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "disposable/twin/composition"
|
2
|
+
|
3
|
+
module Reform::Form::Composition
|
4
|
+
# Automatically creates a Composition object for you when initializing the form.
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
# extend Reform::Form::ActiveModel::ClassMethods # ::model.
|
8
|
+
extend ClassMethods
|
9
|
+
include Disposable::Twin::Composition
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
# Same as ActiveModel::model but allows you to define the main model in the composition
|
15
|
+
# using +:on+.
|
16
|
+
#
|
17
|
+
# class CoverSongForm < Reform::Form
|
18
|
+
# model :song, on: :cover_song
|
19
|
+
def model(main_model, options={})
|
20
|
+
super
|
21
|
+
|
22
|
+
composition_model = options[:on] || main_model
|
23
|
+
|
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|
|
26
|
+
define_method method do
|
27
|
+
model[composition_model].send(method)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "dry-validation"
|
2
|
+
require "dry/validation/schema/form"
|
3
|
+
require "reform/validation"
|
4
|
+
|
5
|
+
module Reform::Form::Dry
|
6
|
+
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
|
20
|
+
end
|
21
|
+
|
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
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Include this in every module that gets further included.
|
2
|
+
module Reform::Form::Module
|
3
|
+
# DISCUSS: could this be part of Declarative?
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.extend Declarative::Heritage::DSL # ::heritage
|
7
|
+
# base.extend Declarative::Heritage::Included # ::included
|
8
|
+
base.extend Included
|
9
|
+
end
|
10
|
+
|
11
|
+
module Included
|
12
|
+
# Gets imported into your module and will be run when including it.
|
13
|
+
def included(includer)
|
14
|
+
super
|
15
|
+
# first, replay all declaratives like ::property on includer.
|
16
|
+
heritage.(includer) # this normally happens via Heritage::Included.
|
17
|
+
# then, include optional accessors.
|
18
|
+
includer.send(:include, self::InstanceMethods) if const_defined?(:InstanceMethods)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def method_missing(method, *args, &block)
|
24
|
+
heritage.record(method, *args, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Reform::Form::Mongoid
|
2
|
+
def self.included(base)
|
3
|
+
base.class_eval do
|
4
|
+
register_feature Reform::Form::Mongoid
|
5
|
+
include Reform::Form::ActiveModel
|
6
|
+
include Reform::Form::ORM
|
7
|
+
extend ClassMethods
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def validates_uniqueness_of(attribute, options={})
|
13
|
+
options = options.merge(:attributes => [attribute])
|
14
|
+
validates_with(UniquenessValidator, options)
|
15
|
+
end
|
16
|
+
def i18n_scope
|
17
|
+
:mongoid
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def self.mongoid_namespace
|
23
|
+
if mongoid_is_4_or_more?
|
24
|
+
'Validatable'
|
25
|
+
else
|
26
|
+
'Validations'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.mongoid_is_4_or_more?
|
31
|
+
Mongoid::VERSION.split('.').first.to_i >= 4
|
32
|
+
end
|
33
|
+
|
34
|
+
UniquenessValidator = Class.new("::Mongoid::#{mongoid_namespace}::UniquenessValidator".constantize) do
|
35
|
+
include Reform::Form::ORM::UniquenessValidator
|
36
|
+
end
|
37
|
+
end
|