reform 2.2.4 → 2.3.3

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 (103) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +5 -1
  3. data/.travis.yml +11 -6
  4. data/Appraisals +8 -0
  5. data/CHANGES.md +57 -4
  6. data/CONTRIBUTING.md +31 -0
  7. data/Gemfile +2 -16
  8. data/ISSUE_TEMPLATE.md +25 -0
  9. data/LICENSE.txt +1 -1
  10. data/README.md +5 -7
  11. data/Rakefile +16 -9
  12. data/gemfiles/0.13.0.gemfile +8 -0
  13. data/gemfiles/1.5.0.gemfile +9 -0
  14. data/lib/reform.rb +1 -0
  15. data/lib/reform/contract.rb +7 -17
  16. data/lib/reform/contract/custom_error.rb +41 -0
  17. data/lib/reform/contract/validate.rb +53 -23
  18. data/lib/reform/errors.rb +61 -0
  19. data/lib/reform/form.rb +36 -10
  20. data/lib/reform/form/call.rb +1 -1
  21. data/lib/reform/form/composition.rb +2 -2
  22. data/lib/reform/form/dry.rb +10 -58
  23. data/lib/reform/form/dry/input_hash.rb +37 -0
  24. data/lib/reform/form/dry/new_api.rb +45 -0
  25. data/lib/reform/form/dry/old_api.rb +61 -0
  26. data/lib/reform/form/populator.rb +11 -27
  27. data/lib/reform/form/prepopulate.rb +4 -3
  28. data/lib/reform/form/validate.rb +28 -13
  29. data/lib/reform/result.rb +90 -0
  30. data/lib/reform/validation.rb +19 -11
  31. data/lib/reform/validation/groups.rb +12 -27
  32. data/lib/reform/version.rb +1 -1
  33. data/reform.gemspec +14 -13
  34. data/test/benchmarking.rb +39 -6
  35. data/test/call_new_api.rb +23 -0
  36. data/test/call_old_api.rb +23 -0
  37. data/test/changed_test.rb +14 -14
  38. data/test/coercion_test.rb +57 -25
  39. data/test/composition_new_api.rb +186 -0
  40. data/test/composition_old_api.rb +184 -0
  41. data/test/contract/custom_error_test.rb +55 -0
  42. data/test/contract_new_api.rb +77 -0
  43. data/test/contract_old_api.rb +77 -0
  44. data/test/default_test.rb +4 -4
  45. data/test/deserialize_test.rb +17 -20
  46. data/test/errors_new_api.rb +225 -0
  47. data/test/errors_old_api.rb +230 -0
  48. data/test/feature_test.rb +10 -12
  49. data/test/fixtures/dry_error_messages.yml +73 -23
  50. data/test/fixtures/dry_new_api_error_messages.yml +104 -0
  51. data/test/form_new_api.rb +57 -0
  52. data/test/{form_test.rb → form_old_api.rb} +8 -8
  53. data/test/form_option_new_api.rb +24 -0
  54. data/test/{form_option_test.rb → form_option_old_api.rb} +5 -5
  55. data/test/from_test.rb +18 -22
  56. data/test/inherit_new_api.rb +105 -0
  57. data/test/inherit_old_api.rb +105 -0
  58. data/test/{module_test.rb → module_new_api.rb} +26 -31
  59. data/test/module_old_api.rb +146 -0
  60. data/test/parse_option_test.rb +40 -0
  61. data/test/parse_pipeline_test.rb +4 -4
  62. data/test/populate_new_api.rb +304 -0
  63. data/test/populate_old_api.rb +304 -0
  64. data/test/populator_skip_test.rb +11 -11
  65. data/test/prepopulator_test.rb +23 -24
  66. data/test/read_only_test.rb +12 -1
  67. data/test/readable_test.rb +9 -9
  68. data/test/reform_new_api.rb +204 -0
  69. data/test/{reform_test.rb → reform_old_api.rb} +44 -65
  70. data/test/save_new_api.rb +101 -0
  71. data/test/save_old_api.rb +101 -0
  72. data/test/setup_test.rb +17 -17
  73. data/test/skip_if_new_api.rb +85 -0
  74. data/test/skip_if_old_api.rb +92 -0
  75. data/test/skip_setter_and_getter_test.rb +9 -10
  76. data/test/test_helper.rb +25 -14
  77. data/test/validate_new_api.rb +453 -0
  78. data/test/{validate_test.rb → validate_old_api.rb} +121 -131
  79. data/test/validation/dry_validation_new_api.rb +835 -0
  80. data/test/validation/dry_validation_old_api.rb +772 -0
  81. data/test/validation/result_test.rb +77 -0
  82. data/test/validation_library_provided_test.rb +16 -0
  83. data/test/virtual_test.rb +47 -7
  84. data/test/writeable_test.rb +38 -9
  85. metadata +111 -56
  86. data/gemfiles/Gemfile.disposable-0.3 +0 -6
  87. data/lib/reform/contract/errors.rb +0 -43
  88. data/lib/reform/form/mongoid.rb +0 -37
  89. data/lib/reform/form/orm.rb +0 -26
  90. data/lib/reform/mongoid.rb +0 -4
  91. data/test/call_test.rb +0 -23
  92. data/test/composition_test.rb +0 -149
  93. data/test/contract_test.rb +0 -77
  94. data/test/deprecation_test.rb +0 -27
  95. data/test/errors_test.rb +0 -165
  96. data/test/inherit_test.rb +0 -119
  97. data/test/populate_test.rb +0 -270
  98. data/test/readonly_test.rb +0 -14
  99. data/test/save_test.rb +0 -89
  100. data/test/skip_if_test.rb +0 -74
  101. data/test/validation/dry_test.rb +0 -60
  102. data/test/validation/dry_validation_test.rb +0 -352
  103. data/test/validation/errors.yml +0 -4
@@ -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]
@@ -21,4 +22,4 @@ private
21
22
  Disposable::Twin::PropertyProcessor.new(dfn, self).() { |form| form.prepopulate!(options) }
22
23
  end
23
24
  end
24
- end
25
+ end
@@ -5,56 +5,71 @@ 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
- properties = options[:binding][:nested].definitions.collect { |dfn| dfn[:name] }
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
- properties.each { |name| (!params[name].nil? && params[name] != "") and return false }
13
- true # skip
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.
27
+ @input_params = params # we want to store these for access via dry later
21
28
  block_given? ? yield(params) : deserialize(params)
22
29
 
23
30
  super() # run the actual validation on self.
24
31
  end
32
+ attr_reader :input_params # make the raw input params public
25
33
 
26
34
  def deserialize(params)
27
35
  params = deserialize!(params)
28
36
  deserializer.new(self).from_hash(params)
29
37
  end
30
38
 
31
- private
39
+ private
40
+
32
41
  # Meant to return params processable by the representer. This is the hook for munching date fields, etc.
33
42
  def deserialize!(params)
34
43
  # NOTE: it is completely up to the form user how they want to deserialize (e.g. using an external JSON-API representer).
35
- # use the deserializer as an external instance to operate on the Twin API,
36
- # e.g. adding new items in collections using #<< etc.
44
+ # use the deserializer as an external instance to operate on the Twin API,
45
+ # e.g. adding new items in collections using #<< etc.
37
46
  # DISCUSS: using self here will call the form's setters like title= which might be overridden.
38
47
  params
39
48
  end
40
49
 
41
50
  # Default deserializer for hash.
42
51
  # This is input-specific, e.g. Hash, JSON, or XML.
43
- def deserializer(source=self.class, options={}) # called on top-level, only, for now.
44
- deserializer = Disposable::Rescheme.from(source,
52
+ def deserializer!(source = self.class, options = {}) # called on top-level, only, for now.
53
+ deserializer = Disposable::Rescheme.from(
54
+ source,
45
55
  {
46
56
  include: [Representable::Hash::AllowSymbols, Representable::Hash],
47
57
  superclass: Representable::Decorator,
48
- definitions_from: lambda { |inline| inline.definitions },
58
+ definitions_from: ->(inline) { inline.definitions },
49
59
  options_from: :deserializer,
50
- exclude_options: [:default, :populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
60
+ exclude_options: %i[default populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
51
61
  }.merge(options)
52
62
  )
53
63
 
54
64
  deserializer
55
65
  end
56
66
 
67
+ def deserializer(*args)
68
+ # DISCUSS: should we simply delegate to class and sort out memoizing there?
69
+ self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
70
+ end
57
71
 
58
- class DeserializeError < RuntimeError
72
+ def self.included(includer)
73
+ includer.singleton_class.send :attr_accessor, :deserializer_class
59
74
  end
60
75
  end
@@ -0,0 +1,90 @@
1
+ module Reform
2
+ class Contract < Disposable::Twin
3
+ # Collects all native results of a form of all groups and provides
4
+ # a unified API: #success?, #errors, #messages, #hints.
5
+ # #success? returns validity of the branch.
6
+ class Result
7
+ def initialize(results, nested_results = []) # DISCUSS: do we like this?
8
+ @results = results # native Result objects, e.g. `#<Dry::Validation::Result output={:title=>"Fallout", :composer=>nil} errors={}>`
9
+ @failure = (results + nested_results).find(&:failure?) # TODO: test nested.
10
+ end
11
+
12
+ def failure?; @failure end
13
+
14
+ def success?; !failure? end
15
+
16
+ def errors(*args); filter_for(:errors, *args) end
17
+
18
+ def messages(*args); filter_for(:messages, *args) end
19
+
20
+ def hints(*args); filter_for(:hints, *args) end
21
+
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. )
33
+ def filter_for(method, *args)
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
43
+ end
44
+
45
+ # Note: this class will be redundant in Reform 3, where the public API
46
+ # allows/enforces to pass options to #errors (e.g. errors(locale: "br"))
47
+ # which means we don't have to "lazy-handle" that with "pointers".
48
+ # :private:
49
+ class Pointer
50
+ extend Forwardable
51
+
52
+ def initialize(result, path)
53
+ @result, @path = result, path
54
+ end
55
+
56
+ def_delegators :@result, :success?, :failure?
57
+
58
+ def errors(*args); traverse_for(:errors, *args) end
59
+
60
+ def messages(*args); traverse_for(:messages, *args) end
61
+
62
+ def hints(*args); traverse_for(:hints, *args) end
63
+
64
+ def advance(*path)
65
+ path = @path + path.compact # remove index if nil.
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)
74
+
75
+ Pointer.new(@result, path)
76
+ end
77
+
78
+ private
79
+
80
+ def traverse(hash, path)
81
+ path.inject(hash) { |errs, segment| errs[segment] || {} } # FIXME. test if all segments present.
82
+ end
83
+
84
+ def traverse_for(method, *args)
85
+ traverse(@result.public_send(method, *args), @path) # TODO: return [] if nil
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -3,28 +3,34 @@
3
3
  module Reform::Validation
4
4
  module ClassMethods
5
5
  def validation_groups
6
- @groups ||= Groups.new(validation_group_class) # TODO: inheritable_attr with Inheritable::Hash
6
+ @groups ||= Groups.new(validation_group_class)
7
7
  end
8
8
 
9
9
  # DSL.
10
- def validation(name=:default, options={}, &block)
11
- heritage.record(:validation, name, options, &block)
10
+ def validation(name = nil, options = {}, &block)
11
+ options = deprecate_validation_positional_args(name, options)
12
+ name = options[:name] # TODO: remove in favor of kw args in 3.0.
12
13
 
14
+ heritage.record(:validation, options, &block)
13
15
  group = validation_groups.add(name, options)
14
16
 
15
17
  group.instance_exec(&block)
16
18
  end
17
19
 
18
- def validates(*args, &block)
19
- validation(:default, inherit: true) { validates *args, &block }
20
- end
20
+ def deprecate_validation_positional_args(name, options)
21
+ if name.is_a?(Symbol)
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 {name: name}.merge(options)
24
+ end
25
+
26
+ return {name: :default}.merge(options) if name.nil?
21
27
 
22
- def validate(*args, &block)
23
- validation(:default, inherit: true) { validate *args, &block }
28
+ {name: :default}.merge(name)
24
29
  end
25
30
 
26
- def validates_with(*args, &block)
27
- validation(:default, inherit: true) { validates_with *args, &block }
31
+ def validation_group_class
32
+ raise NoValidationLibraryError, "no validation library loaded. Please include a " +
33
+ "validation library such as Reform::Form::Dry"
28
34
  end
29
35
  end
30
36
 
@@ -33,8 +39,10 @@ module Reform::Validation
33
39
  end
34
40
 
35
41
  def valid?
36
- Groups::Result.new(self.class.validation_groups).(to_nested_hash, errors, self)
42
+ validate({})
37
43
  end
44
+
45
+ NoValidationLibraryError = Class.new(RuntimeError)
38
46
  end
39
47
 
40
48
  require "reform/validation/groups"
@@ -19,11 +19,11 @@ module Reform::Validation
19
19
 
20
20
  i = index_for(options)
21
21
 
22
- self.insert(i, [name, group = @group_class.new, options]) # Group.new
22
+ self.insert(i, [name, group = @group_class.new(options), options]) # Group.new
23
23
  group
24
24
  end
25
25
 
26
- private
26
+ private
27
27
 
28
28
  def index_for(options)
29
29
  return find_index { |el| el.first == options[:after] } + 1 if options[:after]
@@ -31,43 +31,28 @@ module Reform::Validation
31
31
  end
32
32
 
33
33
  def [](name)
34
- cfg = find { |cfg| cfg.first == name }
34
+ cfg = find { |c| c.first == name }
35
35
  return unless cfg
36
36
  cfg[1]
37
37
  end
38
38
 
39
-
40
- # Runs all validations groups according to their rules and returns result.
41
- # Populates errors passed into #call.
42
- class Result # DISCUSS: could be in Groups.
43
- def initialize(groups)
44
- @groups = groups
45
- end
46
-
47
- def call(fields, errors, form)
48
- result = true
39
+ # Runs all validations groups according to their rules and returns all Result objects.
40
+ class Validate
41
+ def self.call(groups, form)
49
42
  results = {}
50
43
 
51
- @groups.each do |cfg|
52
- name, group, options = cfg
53
- depends_on = options[:if]
44
+ groups.collect do |(name, group, options)|
45
+ next unless evaluate?(options[:if], results, form)
54
46
 
55
- if evaluate_if(depends_on, results, form)
56
- # puts "evaluating #{group.instance_variable_get(:@validator).instance_variable_get(:@checker).inspect}"
57
- results[name] = group.(fields, errors, form).empty? # validate.
58
- end
59
-
60
- result &= errors.empty?
47
+ results[name] = group.(form) # run validation for group. store and collect <Result>.
61
48
  end
62
-
63
- result
64
49
  end
65
50
 
66
- def evaluate_if(depends_on, results, form)
51
+ def self.evaluate?(depends_on, results, form)
67
52
  return true if depends_on.nil?
68
- return results[depends_on] if depends_on.is_a?(Symbol)
53
+ return !results[depends_on].nil? && results[depends_on].success? if depends_on.is_a?(Symbol)
69
54
  form.instance_exec(results, &depends_on)
70
55
  end
71
56
  end
72
57
  end
73
- end
58
+ end
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "2.2.4"
2
+ VERSION = "2.3.3".freeze
3
3
  end
@@ -1,29 +1,30 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path("lib", __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'reform/version'
3
+ require "reform/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "reform"
7
7
  spec.version = Reform::VERSION
8
- spec.authors = ["Nick Sutterer", "Garrett Heinlen"]
9
- spec.email = ["apotonick@gmail.com", "heinleng@gmail.com"]
10
- spec.description = %q{Form object decoupled from models.}
11
- spec.summary = %q{Form object decoupled from models with validation, population and presentation.}
12
- spec.homepage = "https://github.com/apotonick/reform"
8
+ spec.authors = ["Nick Sutterer", "Fran Worley"]
9
+ spec.email = ["apotonick@gmail.com", "frances@safetytoolbox.co.uk"]
10
+ spec.description = "Form object decoupled from models."
11
+ spec.summary = "Form object decoupled from models with validation, population and presentation."
12
+ spec.homepage = "https://github.com/trailblazer/reform"
13
13
  spec.license = "MIT"
14
14
 
15
15
  spec.files = `git ls-files`.split($/)
16
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.executables = spec.files.grep(%r(^bin/)) { |f| File.basename(f) }
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.add_dependency "disposable", ">= 0.4.1"
21
- spec.add_dependency "representable", ">= 2.4.0", "< 3.1.0"
20
+ spec.add_dependency "disposable", ">= 0.4.2", "< 0.5.0"
21
+ spec.add_dependency "representable", ">= 2.4.0", "< 3.1.0"
22
+ spec.add_dependency "uber", "< 0.2.0"
22
23
 
23
24
  spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake"
25
25
  spec.add_development_dependency "minitest"
26
- spec.add_development_dependency "dry-types"
26
+ spec.add_development_dependency "minitest-line"
27
+ spec.add_development_dependency "pry-byebug"
27
28
  spec.add_development_dependency "multi_json"
28
- spec.add_development_dependency "dry-validation", ">= 0.10.0"
29
+ spec.add_development_dependency "rake"
29
30
  end
@@ -1,15 +1,48 @@
1
- require 'reform'
2
- require 'ostruct'
3
- require 'benchmark'
1
+ require "reform"
2
+ require "benchmark/ips"
3
+ require "reform/form/dry"
4
4
 
5
5
  class BandForm < Reform::Form
6
- property :name, validates: {presence: true}
6
+ feature Reform::Form::Dry
7
+ property :name #, validates: {presence: true}
8
+ collection :songs do
9
+ property :title #, validates: {presence: true}
10
+ end
11
+ end
7
12
 
13
+ class OptimizedBandForm < Reform::Form
14
+ feature Reform::Form::Dry
15
+ property :name #, validates: {presence: true}
8
16
  collection :songs do
9
- property :title, validates: {presence: true}
17
+ property :title #, validates: {presence: true}
18
+
19
+ def deserializer(*args)
20
+ # DISCUSS: should we simply delegate to class and sort out memoizing there?
21
+ self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
22
+ end
23
+ end
24
+
25
+ def deserializer(*args)
26
+ # DISCUSS: should we simply delegate to class and sort out memoizing there?
27
+ self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
10
28
  end
11
29
  end
12
30
 
31
+ songs = 10.times.collect { OpenStruct.new(title: "Be Stag") }
32
+ band = OpenStruct.new(name: "Teenage Bottlerock", songs: songs)
33
+
34
+ unoptimized_form = BandForm.new(band)
35
+ optimized_form = OptimizedBandForm.new(band)
36
+
37
+ songs_params = songs_params = 10.times.collect { {title: "Commando"} }
38
+
39
+ Benchmark.ips do |x|
40
+ x.report("2.2") { BandForm.new(band).validate("name" => "Ramones", "songs" => songs_params) }
41
+ x.report("2.3") { OptimizedBandForm.new(band).validate("name" => "Ramones", "songs" => songs_params) }
42
+ end
43
+
44
+ exit
45
+
13
46
  songs = 50.times.collect { OpenStruct.new(title: "Be Stag") }
14
47
  band = OpenStruct.new(name: "Teenage Bottlerock", songs: songs)
15
48
 
@@ -23,4 +56,4 @@ time = Benchmark.measure do
23
56
  end
24
57
  end
25
58
 
26
- puts time
59
+ puts time
@@ -0,0 +1,23 @@
1
+ require "test_helper"
2
+
3
+ class CallTest < Minitest::Spec
4
+ Song = Struct.new(:title)
5
+
6
+ class SongForm < TestForm
7
+ property :title
8
+
9
+ validation do
10
+ params { required(:title).filled }
11
+ end
12
+ end
13
+
14
+ let(:form) { SongForm.new(Song.new) }
15
+
16
+ it { _(form.(title: "True North").success?).must_equal true }
17
+ it { _(form.(title: "True North").failure?).must_equal false }
18
+ it { _(form.(title: "").success?).must_equal false }
19
+ it { _(form.(title: "").failure?).must_equal true }
20
+
21
+ it { _(form.(title: "True North").errors.messages).must_equal({}) }
22
+ it { _(form.(title: "").errors.messages).must_equal(title: ["must be filled"]) }
23
+ end