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