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.
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