reform 2.2.4 → 2.3.0.rc1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +13 -7
  3. data/CHANGES.md +26 -4
  4. data/CONTRIBUTING.md +31 -0
  5. data/Gemfile +1 -12
  6. data/ISSUE_TEMPLATE.md +25 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +3 -3
  9. data/lib/reform.rb +1 -0
  10. data/lib/reform/contract.rb +1 -11
  11. data/lib/reform/contract/validate.rb +49 -23
  12. data/lib/reform/errors.rb +49 -0
  13. data/lib/reform/form.rb +20 -5
  14. data/lib/reform/form/dry.rb +57 -29
  15. data/lib/reform/form/populator.rb +2 -16
  16. data/lib/reform/form/prepopulate.rb +1 -1
  17. data/lib/reform/form/validate.rb +10 -2
  18. data/lib/reform/result.rb +63 -0
  19. data/lib/reform/validation.rb +19 -13
  20. data/lib/reform/validation/groups.rb +11 -25
  21. data/lib/reform/version.rb +1 -1
  22. data/reform.gemspec +7 -6
  23. data/test/benchmarking.rb +39 -5
  24. data/test/call_test.rb +1 -1
  25. data/test/changed_test.rb +1 -1
  26. data/test/coercion_test.rb +2 -2
  27. data/test/composition_test.rb +47 -9
  28. data/test/contract_test.rb +5 -5
  29. data/test/default_test.rb +1 -1
  30. data/test/deserialize_test.rb +3 -3
  31. data/test/errors_test.rb +36 -21
  32. data/test/feature_test.rb +1 -1
  33. data/test/fixtures/dry_error_messages.yml +70 -23
  34. data/test/form_option_test.rb +3 -3
  35. data/test/form_test.rb +3 -3
  36. data/test/from_test.rb +2 -2
  37. data/test/inherit_test.rb +44 -51
  38. data/test/module_test.rb +12 -12
  39. data/test/parse_option_test.rb +40 -0
  40. data/test/parse_pipeline_test.rb +2 -2
  41. data/test/populate_test.rb +59 -19
  42. data/test/populator_skip_test.rb +9 -8
  43. data/test/prepopulator_test.rb +3 -3
  44. data/test/readable_test.rb +2 -2
  45. data/test/readonly_test.rb +1 -1
  46. data/test/reform_test.rb +16 -31
  47. data/test/save_test.rb +23 -8
  48. data/test/setup_test.rb +2 -2
  49. data/test/skip_if_test.rb +4 -4
  50. data/test/skip_setter_and_getter_test.rb +1 -1
  51. data/test/test_helper.rb +13 -10
  52. data/test/validate_test.rb +18 -18
  53. data/test/validation/dry_validation_test.rb +430 -117
  54. data/test/validation/result_test.rb +79 -0
  55. data/test/validation_library_provided_test.rb +16 -0
  56. data/test/virtual_test.rb +1 -1
  57. data/test/writeable_test.rb +31 -2
  58. metadata +42 -23
  59. data/gemfiles/Gemfile.disposable-0.3 +0 -6
  60. data/lib/reform/contract/errors.rb +0 -43
  61. data/lib/reform/form/mongoid.rb +0 -37
  62. data/lib/reform/form/orm.rb +0 -26
  63. data/lib/reform/mongoid.rb +0 -4
  64. data/test/deprecation_test.rb +0 -27
  65. data/test/validation/dry_test.rb +0 -60
  66. data/test/validation/errors.yml +0 -4
@@ -7,7 +7,7 @@
7
7
  class Reform::Form::Populator
8
8
  def initialize(user_proc)
9
9
  @user_proc = user_proc # the actual `populator: ->{}` block from the user, via ::property.
10
- @value = Declarative::Option(user_proc, instance_exec: true) # we can now process Callable, procs, :symbol.
10
+ @value = Declarative::Option(user_proc, instance_exec: true, callable: Object) # we can now process Callable, procs, :symbol.
11
11
  end
12
12
 
13
13
  def call(input, options)
@@ -29,10 +29,7 @@ class Reform::Form::Populator
29
29
  private
30
30
  def call!(options)
31
31
  form = options[:represented]
32
-
33
- deprecate_positional_args(form, @user_proc, options) do
34
- @value.(form, options)
35
- end
32
+ @value.(form, options) # Declarative::Option call.
36
33
  end
37
34
 
38
35
  def handle_fail(twin, options)
@@ -43,17 +40,6 @@ private
43
40
  Representable::GetValue.(nil, options)
44
41
  end
45
42
 
46
- def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
47
- arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
48
- return yield if arity == 1
49
- warn "[Reform] Positional arguments for :populator and friends are deprecated. Please use ->(options) and enjoy the rest of your day. Learn more at http://trailblazerb.org/gems/reform/upgrading-guide.html#to-21"
50
- args = []
51
- args << options[:index] if options[:index]
52
- args << options[:representable_options]
53
- form.instance_exec(options[:fragment], options[:model], *args, &proc)
54
- end
55
-
56
-
57
43
  class IfEmpty < self # Populator
58
44
  def call!(options)
59
45
  binding, twin, index, fragment = options[:binding], options[:model], options[:index], options[:fragment] # TODO: remove once we drop 2.0.
@@ -21,4 +21,4 @@ private
21
21
  Disposable::Twin::PropertyProcessor.new(dfn, self).() { |form| form.prepopulate!(options) }
22
22
  end
23
23
  end
24
- end
24
+ end
@@ -18,16 +18,19 @@ module Reform::Form::Validate
18
18
 
19
19
  def validate(params)
20
20
  # allow an external deserializer.
21
+ @input_params = params # we want to store these for access via dry later
21
22
  block_given? ? yield(params) : deserialize(params)
22
23
 
23
24
  super() # run the actual validation on self.
24
25
  end
26
+ attr_reader :input_params # make the raw input params public
25
27
 
26
28
  def deserialize(params)
27
29
  params = deserialize!(params)
28
30
  deserializer.new(self).from_hash(params)
29
31
  end
30
32
 
33
+
31
34
  private
32
35
  # Meant to return params processable by the representer. This is the hook for munching date fields, etc.
33
36
  def deserialize!(params)
@@ -40,7 +43,7 @@ private
40
43
 
41
44
  # Default deserializer for hash.
42
45
  # This is input-specific, e.g. Hash, JSON, or XML.
43
- def deserializer(source=self.class, options={}) # called on top-level, only, for now.
46
+ def deserializer!(source=self.class, options={}) # called on top-level, only, for now.
44
47
  deserializer = Disposable::Rescheme.from(source,
45
48
  {
46
49
  include: [Representable::Hash::AllowSymbols, Representable::Hash],
@@ -54,7 +57,12 @@ private
54
57
  deserializer
55
58
  end
56
59
 
60
+ def deserializer(*args)
61
+ # DISCUSS: should we simply delegate to class and sort out memoizing there?
62
+ self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
63
+ end
57
64
 
58
- class DeserializeError < RuntimeError
65
+ def self.included(includer)
66
+ includer.singleton_class.send :attr_accessor, :deserializer_class
59
67
  end
60
68
  end
@@ -0,0 +1,63 @@
1
+ module Reform
2
+ class Contract < Disposable::Twin
3
+
4
+ # Collects all native results of a form of all groups and provides
5
+ # a unified API: #success?, #errors, #messages, #hints.
6
+ # #success? returns validity of the branch.
7
+ class Result
8
+ def initialize(results, nested_results=[]) # DISCUSS: do we like this?
9
+ @results = results # native Result objects, e.g. `#<Dry::Validation::Result output={:title=>"Fallout", :composer=>nil} errors={}>`
10
+ @failure = (results + nested_results).find(&:failure?) # TODO: test nested.
11
+ end
12
+
13
+ def failure?; @failure end
14
+ def success?; !failure? end
15
+
16
+ def errors(*args); filter_for(:errors, *args) end
17
+ def messages(*args); filter_for(:messages, *args) end
18
+ def hints(*args); filter_for(:hints, *args) end
19
+
20
+ private
21
+ def filter_for(method, *args)
22
+ @results.collect { |r| r.public_send(method, *args) }
23
+ .inject({}) { |hsh, errs| hsh.merge(errs) }
24
+ .find_all { |k, v| v.is_a?(Array) } # filter :nested=>{:something=>["too nested!"]} #DISCUSS: do we want that here?
25
+ .to_h
26
+ end
27
+
28
+ # Note: this class will be redundant in Reform 3, where the public API
29
+ # allows/enforces to pass options to #errors (e.g. errors(locale: "br"))
30
+ # which means we don't have to "lazy-handle" that with "pointers".
31
+ # :private:
32
+ class Pointer
33
+ extend Forwardable
34
+
35
+ def initialize(result, path)
36
+ @result, @path = result, path
37
+ end
38
+
39
+ def_delegators :@result, :success?, :failure?
40
+
41
+ def errors(*args); traverse_for(:errors, *args) end
42
+ def messages(*args); traverse_for(:messages, *args) end
43
+ def hints(*args); traverse_for(:hints, *args) end
44
+
45
+ def advance(*path)
46
+ path = @path + path.compact # remove index if nil.
47
+ return if traverse(@result.errors, path) == {}
48
+
49
+ Pointer.new(@result, path)
50
+ end
51
+
52
+ private
53
+ def traverse(hash, path)
54
+ path.inject(hash) { |errs, segment| errs[segment] || {} } # FIXME. test if all segments present.
55
+ end
56
+
57
+ def traverse_for(method, *args)
58
+ traverse(@result.public_send(method, *args), @path) # TODO: return [] if nil
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -3,28 +3,36 @@
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
+ if name.nil?
27
+ return { name: :default }.merge(options)
28
+ end
21
29
 
22
- def validate(*args, &block)
23
- validation(:default, inherit: true) { validate *args, &block }
30
+ { name: :default }.merge(name)
24
31
  end
25
32
 
26
- def validates_with(*args, &block)
27
- validation(:default, inherit: true) { validates_with *args, &block }
33
+ def validation_group_class
34
+ raise NoValidationLibraryError, 'no validation library loaded. Please include a ' +
35
+ 'validation library such as Reform::Form::Dry'
28
36
  end
29
37
  end
30
38
 
@@ -32,9 +40,7 @@ module Reform::Validation
32
40
  includer.extend(ClassMethods)
33
41
  end
34
42
 
35
- def valid?
36
- Groups::Result.new(self.class.validation_groups).(to_nested_hash, errors, self)
37
- end
43
+ NoValidationLibraryError = Class.new(RuntimeError)
38
44
  end
39
45
 
40
46
  require "reform/validation/groups"
@@ -19,7 +19,7 @@ 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
 
@@ -31,43 +31,29 @@ 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
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
40
+ # Runs all validations groups according to their rules and returns all Result objects.
41
+ class Validate
42
+ def self.call(groups, form)
49
43
  results = {}
50
44
 
51
- @groups.each do |cfg|
52
- name, group, options = cfg
53
- depends_on = options[:if]
45
+ groups.collect do |(name, group, options)|
46
+ next unless evaluate?(options[:if], results, form)
54
47
 
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?
48
+ results[name] = group.(form) # run validation for group. store and collect <Result>.
61
49
  end
62
-
63
- result
64
50
  end
65
51
 
66
- def evaluate_if(depends_on, results, form)
52
+ def self.evaluate?(depends_on, results, form)
67
53
  return true if depends_on.nil?
68
- return results[depends_on] if depends_on.is_a?(Symbol)
54
+ return results[depends_on].success? if depends_on.is_a?(Symbol)
69
55
  form.instance_exec(results, &depends_on)
70
56
  end
71
57
  end
72
58
  end
73
- end
59
+ end
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "2.2.4"
2
+ VERSION = "2.3.0.rc1"
3
3
  end
@@ -5,11 +5,11 @@ require 'reform/version'
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"]
8
+ spec.authors = ["Nick Sutterer", "Fran Worley"]
9
+ spec.email = ["apotonick@gmail.com", "frances@safetytoolbox.co.uk"]
10
10
  spec.description = %q{Form object decoupled from models.}
11
11
  spec.summary = %q{Form object decoupled from models with validation, population and presentation.}
12
- spec.homepage = "https://github.com/apotonick/reform"
12
+ spec.homepage = "https://github.com/trailblazer/reform"
13
13
  spec.license = "MIT"
14
14
 
15
15
  spec.files = `git ls-files`.split($/)
@@ -17,13 +17,14 @@ Gem::Specification.new do |spec|
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 "uber", "< 0.2.0"
22
+ spec.add_dependency "representable", ">= 2.4.0", "< 3.1.0"
22
23
 
23
24
  spec.add_development_dependency "bundler"
24
25
  spec.add_development_dependency "rake"
25
26
  spec.add_development_dependency "minitest"
26
27
  spec.add_development_dependency "dry-types"
27
28
  spec.add_development_dependency "multi_json"
28
- spec.add_development_dependency "dry-validation", ">= 0.10.0"
29
+ spec.add_development_dependency "dry-validation", ">= 0.10.1"
29
30
  end
@@ -1,15 +1,49 @@
1
1
  require 'reform'
2
- require 'ostruct'
3
- require 'benchmark'
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
+
46
+
13
47
  songs = 50.times.collect { OpenStruct.new(title: "Be Stag") }
14
48
  band = OpenStruct.new(name: "Teenage Bottlerock", songs: songs)
15
49
 
@@ -23,4 +57,4 @@ time = Benchmark.measure do
23
57
  end
24
58
  end
25
59
 
26
- puts time
60
+ puts time
@@ -3,7 +3,7 @@ require "test_helper"
3
3
  class CallTest < Minitest::Spec
4
4
  Song = Struct.new(:title)
5
5
 
6
- class SongForm < Reform::Form
6
+ class SongForm < TestForm
7
7
  property :title
8
8
 
9
9
  validation do
@@ -6,7 +6,7 @@ class ChangedTest < MiniTest::Spec
6
6
  Album = Struct.new(:name, :songs, :artist)
7
7
  Artist = Struct.new(:name)
8
8
 
9
- class AlbumForm < Reform::Form
9
+ class AlbumForm < TestForm
10
10
  property :name
11
11
 
12
12
  collection :songs do
@@ -8,7 +8,7 @@ class CoercionTest < BaseTest
8
8
  end
9
9
  end
10
10
 
11
- class Form < Reform::Form
11
+ class Form < TestForm
12
12
  feature Coercion
13
13
 
14
14
  property :released_at, type: Types::Form::DateTime
@@ -58,7 +58,7 @@ class CoercionTest < BaseTest
58
58
 
59
59
  it { subject.released_at.must_equal DateTime.parse("30/03/1981") }
60
60
  it { subject.hit.length.must_equal 312 }
61
- it { subject.hit.good.must_equal nil }
61
+ it { assert_nil subject.hit.good }
62
62
  it { subject.band.label.value.must_equal "9999.999999.99" } # coercion happened once.
63
63
  end
64
64
 
@@ -1,11 +1,49 @@
1
1
  require 'test_helper'
2
2
 
3
+ class FormCompositionInheritanceTest < MiniTest::Spec
4
+ module SizePrice
5
+ include Reform::Form::Module
6
+
7
+ property :price
8
+ property :size
9
+
10
+ module InstanceMethods
11
+ def price(for_size: size)
12
+ case for_size.to_sym
13
+ when :s then super() * 1
14
+ when :m then super() * 2
15
+ when :l then super() * 3
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ class OutfitForm < TestForm
22
+ include Reform::Form::Composition
23
+ include SizePrice
24
+
25
+ property :price, inherit: true, on: :tshirt
26
+ property :size, inherit: true, on: :measurement
27
+ end
28
+
29
+
30
+ let (:measurement) { Measurement.new(:l) }
31
+ let (:tshirt) { Tshirt.new(2, :m) }
32
+ let (:form) { OutfitForm.new(tshirt: tshirt, measurement: measurement) }
33
+
34
+ Tshirt = Struct.new(:price, :size)
35
+ Measurement = Struct.new(:size)
36
+
37
+ it { form.price.must_equal 6 }
38
+ it { form.price(for_size: :s).must_equal 2 }
39
+ end
40
+
3
41
  class FormCompositionTest < MiniTest::Spec
4
42
  Song = Struct.new(:id, :title, :band)
5
43
  Requester = Struct.new(:id, :name, :requester)
6
44
  Band = Struct.new(:title)
7
45
 
8
- class RequestForm < Reform::Form
46
+ class RequestForm < TestForm
9
47
  include Composition
10
48
 
11
49
  property :name, :on => :requester
@@ -17,9 +55,8 @@ class FormCompositionTest < MiniTest::Spec
17
55
  property :captcha, :on => :song, :virtual => true
18
56
 
19
57
  validation do
20
- key(:name).required
21
- key(:name).required
22
- key(:title).required
58
+ required(:name).filled
59
+ required(:title).filled
23
60
  end
24
61
 
25
62
  property :band, :on => :song do
@@ -37,20 +74,21 @@ class FormCompositionTest < MiniTest::Spec
37
74
  it { form.title.must_equal "Rio" }
38
75
  it { form.name.must_equal "Duran Duran" }
39
76
  it { form.requester_id.must_equal 2 }
40
- it { form.channel.must_equal nil }
77
+ it { assert_nil form.channel }
41
78
  it { form.requester.must_equal "MCP" } # same name as composed model.
42
- it { form.captcha.must_equal nil }
79
+ it { assert_nil form.captcha }
43
80
 
44
81
  # #model just returns <Composition>.
45
82
  it { form.mapper.must_be_kind_of Disposable::Composition }
46
83
 
47
84
  # #model[] -> composed models
48
85
  it { form.model[:requester].must_equal requester }
49
- it { form.model[:song].must_equal song }
86
+ it { form.model[:song].must_equal song }
50
87
 
51
88
 
52
89
  it "creates Composition for you" do
53
- form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal false
90
+ form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal true
91
+ form.validate("title" => "", "name" => "Frenzal Rhomb").must_equal false
54
92
  end
55
93
 
56
94
  describe "#save" do
@@ -133,7 +171,7 @@ class FormCompositionCollectionTest < MiniTest::Spec
133
171
  end
134
172
  end
135
173
 
136
- class LibraryForm < Reform::Form
174
+ class LibraryForm < TestForm
137
175
  include Reform::Form::Composition
138
176
 
139
177
  collection :books, on: :library do