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
@@ -0,0 +1,57 @@
1
+ require "test_helper"
2
+
3
+ class FormTest < MiniTest::Spec
4
+ Artist = Struct.new(:name)
5
+
6
+ class AlbumForm < TestForm
7
+ property :title
8
+
9
+ property :hit do
10
+ property :title
11
+ end
12
+
13
+ collection :songs do
14
+ property :title
15
+ end
16
+
17
+ property :band do # yepp, people do crazy stuff like that.
18
+ property :label do
19
+ property :name
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "::dup" do
25
+ let(:cloned) { AlbumForm.clone }
26
+
27
+ # #dup is called in Op.inheritable_attr(:contract_class), it must be subclass of the original one.
28
+ it { _(cloned).wont_equal AlbumForm }
29
+ it { _(AlbumForm.definitions).wont_equal cloned.definitions }
30
+
31
+ it do
32
+ # currently, forms need a name for validation, even without AM.
33
+ cloned.singleton_class.class_eval do
34
+ def name
35
+ "Album"
36
+ end
37
+ end
38
+
39
+ cloned.validation do
40
+ params { required(:title).filled }
41
+ end
42
+
43
+ cloned.new(OpenStruct.new).validate({})
44
+ end
45
+ end
46
+
47
+ describe "#initialize" do
48
+ class ArtistForm < TestForm
49
+ property :name
50
+ property :current_user, virtual: true
51
+ end
52
+
53
+ it "allows injecting :virtual options" do
54
+ _(ArtistForm.new(Artist.new, current_user: Object).current_user).must_equal Object
55
+ end
56
+ end
57
+ end
@@ -1,9 +1,9 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
 
3
3
  class FormTest < MiniTest::Spec
4
4
  Artist = Struct.new(:name)
5
5
 
6
- class AlbumForm < Reform::Form
6
+ class AlbumForm < TestForm
7
7
  property :title
8
8
 
9
9
  property :hit do
@@ -22,11 +22,11 @@ class FormTest < MiniTest::Spec
22
22
  end
23
23
 
24
24
  describe "::dup" do
25
- let (:cloned) { AlbumForm.clone }
25
+ let(:cloned) { AlbumForm.clone }
26
26
 
27
27
  # #dup is called in Op.inheritable_attr(:contract_class), it must be subclass of the original one.
28
- it { cloned.wont_equal AlbumForm }
29
- it { AlbumForm.definitions.wont_equal cloned.definitions }
28
+ it { _(cloned).wont_equal AlbumForm }
29
+ it { _(AlbumForm.definitions).wont_equal cloned.definitions }
30
30
 
31
31
  it do
32
32
  # currently, forms need a name for validation, even without AM.
@@ -37,7 +37,7 @@ class FormTest < MiniTest::Spec
37
37
  end
38
38
 
39
39
  cloned.validation do
40
- key(:title).required
40
+ required(:title).filled
41
41
  end
42
42
 
43
43
  cloned.new(OpenStruct.new).validate({})
@@ -45,13 +45,13 @@ class FormTest < MiniTest::Spec
45
45
  end
46
46
 
47
47
  describe "#initialize" do
48
- class ArtistForm < Reform::Form
48
+ class ArtistForm < TestForm
49
49
  property :name
50
50
  property :current_user, virtual: true
51
51
  end
52
52
 
53
53
  it "allows injecting :virtual options" do
54
- ArtistForm.new(Artist.new, current_user: Object).current_user.must_equal Object
54
+ _(ArtistForm.new(Artist.new, current_user: Object).current_user).must_equal Object
55
55
  end
56
56
  end
57
57
  end
@@ -0,0 +1,24 @@
1
+ require "test_helper"
2
+
3
+ class FormOptionTest < MiniTest::Spec
4
+ Song = Struct.new(:title)
5
+ Album = Struct.new(:song)
6
+
7
+ class SongForm < TestForm
8
+ property :title
9
+ validation do
10
+ params { required(:title).filled }
11
+ end
12
+ end
13
+
14
+ class AlbumForm < TestForm
15
+ property :song, form: SongForm
16
+ end
17
+
18
+ it do
19
+ form = AlbumForm.new(Album.new(Song.new("When It Comes To You")))
20
+ _(form.song.title).must_equal "When It Comes To You"
21
+
22
+ form.validate(song: {title: "Run For Cover"})
23
+ end
24
+ end
@@ -1,23 +1,23 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
 
3
3
  class FormOptionTest < MiniTest::Spec
4
4
  Song = Struct.new(:title)
5
5
  Album = Struct.new(:song)
6
6
 
7
- class SongForm < Reform::Form
7
+ class SongForm < TestForm
8
8
  property :title
9
9
  validation do
10
- key(:title).required
10
+ required(:title).filled
11
11
  end
12
12
  end
13
13
 
14
- class AlbumForm < Reform::Form
14
+ class AlbumForm < TestForm
15
15
  property :song, form: SongForm
16
16
  end
17
17
 
18
18
  it do
19
19
  form = AlbumForm.new(Album.new(Song.new("When It Comes To You")))
20
- form.song.title.must_equal "When It Comes To You"
20
+ _(form.song.title).must_equal "When It Comes To You"
21
21
 
22
22
  form.validate(song: {title: "Run For Cover"})
23
23
  end
@@ -1,7 +1,7 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
 
3
3
  class AsTest < BaseTest
4
- class AlbumForm < Reform::Form
4
+ class AlbumForm < TestForm
5
5
  property :name, from: :title
6
6
 
7
7
  property :single, from: :hit do
@@ -19,46 +19,42 @@ class AsTest < BaseTest
19
19
  end
20
20
  end
21
21
 
22
- let (:song2) { Song.new("Roxanne") }
22
+ let(:song2) { Song.new("Roxanne") }
23
23
 
24
- let (:params) {
24
+ let(:params) do
25
25
  {
26
26
  "name" => "Best Of The Police",
27
- "single" => {"title" => "So Lonely"},
27
+ "single" => {"title" => "So Lonely"},
28
28
  "tracks" => [{"name" => "Message In A Bottle"}, {"name" => "Roxanne"}]
29
29
  }
30
- }
30
+ end
31
31
 
32
32
  subject { AlbumForm.new(Album.new("Best Of", hit, [Song.new("Fallout"), song2])) }
33
33
 
34
- it { subject.name.must_equal "Best Of" }
35
- it { subject.single.title.must_equal "Roxanne" }
36
- it { subject.tracks[0].name.must_equal "Fallout" }
37
- it { subject.tracks[1].name.must_equal "Roxanne" }
38
-
34
+ it { _(subject.name).must_equal "Best Of" }
35
+ it { _(subject.single.title).must_equal "Roxanne" }
36
+ it { _(subject.tracks[0].name).must_equal "Fallout" }
37
+ it { _(subject.tracks[1].name).must_equal "Roxanne" }
39
38
 
40
39
  describe "#validate" do
41
40
 
42
-
43
41
  before { subject.validate(params) }
44
42
 
45
- it { subject.name.must_equal "Best Of The Police" }
46
- it { subject.single.title.must_equal "So Lonely" }
47
- it { subject.tracks[0].name.must_equal "Message In A Bottle" }
48
- it { subject.tracks[1].name.must_equal "Roxanne" }
43
+ it { _(subject.name).must_equal "Best Of The Police" }
44
+ it { _(subject.single.title).must_equal "So Lonely" }
45
+ it { _(subject.tracks[0].name).must_equal "Message In A Bottle" }
46
+ it { _(subject.tracks[1].name).must_equal "Roxanne" }
49
47
  end
50
48
 
51
-
52
49
  describe "#sync" do
53
- before {
50
+ before do
54
51
  subject.tracks[1].name = "Livin' Ain't No Crime"
55
52
  subject.sync
56
- }
53
+ end
57
54
 
58
- it { song2.title.must_equal "Livin' Ain't No Crime" }
55
+ it { _(song2.title).must_equal "Livin' Ain't No Crime" }
59
56
  end
60
57
 
61
-
62
58
  describe "#save (nested hash)" do
63
59
  before { subject.validate(params) }
64
60
 
@@ -69,7 +65,7 @@ class AsTest < BaseTest
69
65
  hash = nested_hash
70
66
  end
71
67
 
72
- hash.must_equal({"title"=>"Best Of The Police", "hit"=>{"title"=>"So Lonely"}, "songs"=>[{"title"=>"Message In A Bottle"}, {"title"=>"Roxanne"}]})
68
+ _(hash).must_equal({"title" => "Best Of The Police", "hit" => {"title" => "So Lonely"}, "songs" => [{"title" => "Message In A Bottle"}, {"title" => "Roxanne"}], "band" => nil})
73
69
  end
74
70
  end
75
71
  end
@@ -0,0 +1,105 @@
1
+ require "test_helper"
2
+ require "representable/json"
3
+
4
+ class InheritTest < BaseTest
5
+ Populator = Reform::Form::Populator
6
+
7
+ class SkipParse
8
+ include Uber::Callable
9
+ def call(*_args)
10
+ false
11
+ end
12
+ end
13
+
14
+ class AlbumForm < TestForm
15
+ property :title, deserializer: {instance: "Instance"}, skip_if: "skip_if in AlbumForm" # allow direct configuration of :deserializer.
16
+
17
+ property :hit, populate_if_empty: ->(*) { Song.new } do
18
+ property :title
19
+ validation do
20
+ params { required(:title).filled }
21
+ end
22
+ end
23
+
24
+ collection :songs, populate_if_empty: -> {}, skip_if: :all_blank do
25
+ property :title
26
+ end
27
+
28
+ property :band, populate_if_empty: -> {} do
29
+ def band_id
30
+ 1
31
+ end
32
+ end
33
+ end
34
+
35
+ class CompilationForm < AlbumForm
36
+ property :title, inherit: true, skip_if: "skip_if from CompilationForm"
37
+ property :hit, inherit: true, populate_if_empty: ->(*) { Song.new }, skip_if: SkipParse.new do
38
+ property :rating
39
+ validation do
40
+ params { required(:rating).filled }
41
+ end
42
+ end
43
+
44
+ # NO collection here, this is entirely inherited.
45
+
46
+ property :band, inherit: true do # inherit everything, but explicitely.
47
+ end
48
+ end
49
+
50
+ let(:album) { Album.new(nil, Song.new, [], Band.new) }
51
+ subject { CompilationForm.new(album) }
52
+
53
+ it do
54
+ subject.validate("hit" => {"title" => "LA Drone", "rating" => 10})
55
+ _(subject.hit.title).must_equal "LA Drone"
56
+ _(subject.hit.rating).must_equal 10
57
+ _(subject.errors.messages).must_equal({})
58
+ end
59
+
60
+ it do
61
+ subject.validate({})
62
+ assert_nil subject.model.hit.title
63
+ assert_nil subject.model.hit.rating
64
+ _(subject.errors.messages).must_equal("hit.title": ["must be filled"], "hit.rating": ["must be filled"])
65
+ end
66
+
67
+ it "xxx" do
68
+ # sub hashes like :deserializer must be properly cloned when inheriting.
69
+ _(AlbumForm.options_for(:title)[:deserializer].object_id).wont_equal CompilationForm.options_for(:title)[:deserializer].object_id
70
+
71
+ # don't overwrite direct deserializer: {} configuration.
72
+ _(AlbumForm.options_for(:title)[:internal_populator]).must_be_instance_of Reform::Form::Populator::Sync
73
+ _(AlbumForm.options_for(:title)[:deserializer][:skip_parse]).must_equal "skip_if in AlbumForm"
74
+
75
+ # AlbumForm.options_for(:hit)[:internal_populator].inspect.must_match /Reform::Form::Populator:.+ @user_proc="Populator"/
76
+ # AlbumForm.options_for(:hit)[:deserializer][:instance].inspect.must_be_instance_with Reform::Form::Populator, user_proc: "Populator"
77
+
78
+ _(AlbumForm.options_for(:songs)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
79
+ _(AlbumForm.options_for(:songs)[:deserializer][:skip_parse]).must_be_instance_of Reform::Form::Validate::Skip::AllBlank
80
+
81
+ _(AlbumForm.options_for(:band)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
82
+
83
+ _(CompilationForm.options_for(:title)[:deserializer][:skip_parse]).must_equal "skip_if from CompilationForm"
84
+ # pp CompilationForm.options_for(:songs)
85
+ _(CompilationForm.options_for(:songs)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
86
+
87
+ _(CompilationForm.options_for(:band)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
88
+
89
+ # completely overwrite inherited.
90
+ _(CompilationForm.options_for(:hit)[:deserializer][:skip_parse]).must_be_instance_of SkipParse
91
+
92
+ # inherit: true with block will still inherit the original class.
93
+ _(AlbumForm.new(OpenStruct.new(band: OpenStruct.new)).band.band_id).must_equal 1
94
+ _(CompilationForm.new(OpenStruct.new(band: OpenStruct.new)).band.band_id).must_equal 1
95
+ end
96
+
97
+ class CDForm < AlbumForm
98
+ # override :band's original populate_if_empty but with :inherit.
99
+ property :band, inherit: true, populator: "CD Populator" do
100
+
101
+ end
102
+ end
103
+
104
+ it { _(CDForm.options_for(:band)[:internal_populator].instance_variable_get(:@user_proc)).must_equal "CD Populator" }
105
+ end
@@ -0,0 +1,105 @@
1
+ require "test_helper"
2
+ require "representable/json"
3
+
4
+ class InheritTest < BaseTest
5
+ Populator = Reform::Form::Populator
6
+
7
+ class SkipParse
8
+ include Uber::Callable
9
+ def call(*_args)
10
+ false
11
+ end
12
+ end
13
+
14
+ class AlbumForm < TestForm
15
+ property :title, deserializer: {instance: "Instance"}, skip_if: "skip_if in AlbumForm" # allow direct configuration of :deserializer.
16
+
17
+ property :hit, populate_if_empty: ->(*) { Song.new } do
18
+ property :title
19
+ validation do
20
+ required(:title).filled
21
+ end
22
+ end
23
+
24
+ collection :songs, populate_if_empty: -> {}, skip_if: :all_blank do
25
+ property :title
26
+ end
27
+
28
+ property :band, populate_if_empty: -> {} do
29
+ def band_id
30
+ 1
31
+ end
32
+ end
33
+ end
34
+
35
+ class CompilationForm < AlbumForm
36
+ property :title, inherit: true, skip_if: "skip_if from CompilationForm"
37
+ property :hit, inherit: true, populate_if_empty: ->(*) { Song.new }, skip_if: SkipParse.new do
38
+ property :rating
39
+ validation do
40
+ required(:rating).filled
41
+ end
42
+ end
43
+
44
+ # NO collection here, this is entirely inherited.
45
+
46
+ property :band, inherit: true do # inherit everything, but explicitely.
47
+ end
48
+ end
49
+
50
+ let(:album) { Album.new(nil, Song.new, [], Band.new) }
51
+ subject { CompilationForm.new(album) }
52
+
53
+ it do
54
+ subject.validate("hit" => {"title" => "LA Drone", "rating" => 10})
55
+ _(subject.hit.title).must_equal "LA Drone"
56
+ _(subject.hit.rating).must_equal 10
57
+ _(subject.errors.messages).must_equal({})
58
+ end
59
+
60
+ it do
61
+ subject.validate({})
62
+ assert_nil subject.model.hit.title
63
+ assert_nil subject.model.hit.rating
64
+ _(subject.errors.messages).must_equal("hit.title": ["must be filled"], "hit.rating": ["must be filled"])
65
+ end
66
+
67
+ it "xxx" do
68
+ # sub hashes like :deserializer must be properly cloned when inheriting.
69
+ _(AlbumForm.options_for(:title)[:deserializer].object_id).wont_equal CompilationForm.options_for(:title)[:deserializer].object_id
70
+
71
+ # don't overwrite direct deserializer: {} configuration.
72
+ _(AlbumForm.options_for(:title)[:internal_populator]).must_be_instance_of Reform::Form::Populator::Sync
73
+ _(AlbumForm.options_for(:title)[:deserializer][:skip_parse]).must_equal "skip_if in AlbumForm"
74
+
75
+ # AlbumForm.options_for(:hit)[:internal_populator].inspect.must_match /Reform::Form::Populator:.+ @user_proc="Populator"/
76
+ # AlbumForm.options_for(:hit)[:deserializer][:instance].inspect.must_be_instance_with Reform::Form::Populator, user_proc: "Populator"
77
+
78
+ _(AlbumForm.options_for(:songs)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
79
+ _(AlbumForm.options_for(:songs)[:deserializer][:skip_parse]).must_be_instance_of Reform::Form::Validate::Skip::AllBlank
80
+
81
+ _(AlbumForm.options_for(:band)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
82
+
83
+ _(CompilationForm.options_for(:title)[:deserializer][:skip_parse]).must_equal "skip_if from CompilationForm"
84
+ # pp CompilationForm.options_for(:songs)
85
+ _(CompilationForm.options_for(:songs)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
86
+
87
+ _(CompilationForm.options_for(:band)[:internal_populator]).must_be_instance_of Reform::Form::Populator::IfEmpty
88
+
89
+ # completely overwrite inherited.
90
+ _(CompilationForm.options_for(:hit)[:deserializer][:skip_parse]).must_be_instance_of SkipParse
91
+
92
+ # inherit: true with block will still inherit the original class.
93
+ _(AlbumForm.new(OpenStruct.new(band: OpenStruct.new)).band.band_id).must_equal 1
94
+ _(CompilationForm.new(OpenStruct.new(band: OpenStruct.new)).band.band_id).must_equal 1
95
+ end
96
+
97
+ class CDForm < AlbumForm
98
+ # override :band's original populate_if_empty but with :inherit.
99
+ property :band, inherit: true, populator: "CD Populator" do
100
+
101
+ end
102
+ end
103
+
104
+ it { _(CDForm.options_for(:band)[:internal_populator].instance_variable_get(:@user_proc)).must_equal "CD Populator" }
105
+ end