reform 2.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGES.md +415 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +339 -0
  8. data/Rakefile +15 -0
  9. data/TODO.md +45 -0
  10. data/gemfiles/Gemfile.disposable-0.3 +6 -0
  11. data/lib/reform.rb +8 -0
  12. data/lib/reform/contract.rb +77 -0
  13. data/lib/reform/contract/errors.rb +43 -0
  14. data/lib/reform/contract/validate.rb +33 -0
  15. data/lib/reform/form.rb +94 -0
  16. data/lib/reform/form/call.rb +23 -0
  17. data/lib/reform/form/coercion.rb +3 -0
  18. data/lib/reform/form/composition.rb +34 -0
  19. data/lib/reform/form/dry.rb +67 -0
  20. data/lib/reform/form/module.rb +27 -0
  21. data/lib/reform/form/mongoid.rb +37 -0
  22. data/lib/reform/form/orm.rb +26 -0
  23. data/lib/reform/form/populator.rb +123 -0
  24. data/lib/reform/form/prepopulate.rb +24 -0
  25. data/lib/reform/form/validate.rb +60 -0
  26. data/lib/reform/mongoid.rb +4 -0
  27. data/lib/reform/validation.rb +40 -0
  28. data/lib/reform/validation/groups.rb +73 -0
  29. data/lib/reform/version.rb +3 -0
  30. data/reform.gemspec +29 -0
  31. data/test/benchmarking.rb +26 -0
  32. data/test/call_test.rb +23 -0
  33. data/test/changed_test.rb +41 -0
  34. data/test/coercion_test.rb +66 -0
  35. data/test/composition_test.rb +149 -0
  36. data/test/contract_test.rb +77 -0
  37. data/test/default_test.rb +22 -0
  38. data/test/deprecation_test.rb +27 -0
  39. data/test/deserialize_test.rb +104 -0
  40. data/test/errors_test.rb +165 -0
  41. data/test/feature_test.rb +65 -0
  42. data/test/fixtures/dry_error_messages.yml +44 -0
  43. data/test/form_option_test.rb +24 -0
  44. data/test/form_test.rb +57 -0
  45. data/test/from_test.rb +75 -0
  46. data/test/inherit_test.rb +119 -0
  47. data/test/module_test.rb +142 -0
  48. data/test/parse_pipeline_test.rb +15 -0
  49. data/test/populate_test.rb +270 -0
  50. data/test/populator_skip_test.rb +28 -0
  51. data/test/prepopulator_test.rb +112 -0
  52. data/test/read_only_test.rb +3 -0
  53. data/test/readable_test.rb +30 -0
  54. data/test/readonly_test.rb +14 -0
  55. data/test/reform_test.rb +223 -0
  56. data/test/save_test.rb +89 -0
  57. data/test/setup_test.rb +48 -0
  58. data/test/skip_if_test.rb +74 -0
  59. data/test/skip_setter_and_getter_test.rb +54 -0
  60. data/test/test_helper.rb +49 -0
  61. data/test/validate_test.rb +420 -0
  62. data/test/validation/dry_test.rb +60 -0
  63. data/test/validation/dry_validation_test.rb +352 -0
  64. data/test/validation/errors.yml +4 -0
  65. data/test/virtual_test.rb +24 -0
  66. data/test/writeable_test.rb +29 -0
  67. metadata +265 -0
@@ -0,0 +1,8 @@
1
+ module Reform
2
+ end
3
+
4
+ require "disposable"
5
+ require "reform/contract"
6
+ require "reform/form"
7
+ require "reform/form/composition"
8
+ require "reform/form/module"
@@ -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
@@ -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,3 @@
1
+ require "disposable/twin/coercion"
2
+
3
+ Reform::Form::Coercion = Disposable::Twin::Coercion
@@ -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