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