reform 2.2.4 → 2.3.3

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