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,5 +1,5 @@
1
1
  require "test_helper"
2
- require 'reform/form/coercion'
2
+ require "reform/form/coercion"
3
3
 
4
4
  class ModuleInclusionTest < MiniTest::Spec
5
5
  module BandPropertyForm
@@ -9,7 +9,7 @@ class ModuleInclusionTest < MiniTest::Spec
9
9
  property :title
10
10
 
11
11
  validation do
12
- key(:title).required
12
+ params { required(:title).filled }
13
13
  end
14
14
 
15
15
  def id # gets mixed into Form, too.
@@ -22,11 +22,11 @@ class ModuleInclusionTest < MiniTest::Spec
22
22
  end
23
23
 
24
24
  validation do
25
- key(:band).required
25
+ params { required(:band).filled }
26
26
  end
27
27
 
28
28
  include Dry::Types.module # allows using Types::* in module.
29
- property :cool, type: Form::Bool # test coercion.
29
+ property :cool, type: DRY_TYPES_CONSTANT::Bool # test coercion.
30
30
  end
31
31
 
32
32
  # TODO: test if works, move stuff into inherit_schema!
@@ -36,53 +36,50 @@ class ModuleInclusionTest < MiniTest::Spec
36
36
  collection :airplays do
37
37
  property :station
38
38
  validation do
39
- key(:station).required
39
+ params { required(:station).filled }
40
40
  end
41
41
  end
42
42
  validation do
43
- key(:airplays).required
43
+ params { required(:airplays).filled }
44
44
  end
45
45
  end
46
46
 
47
-
48
47
  # test:
49
48
  # by including BandPropertyForm into multiple classes we assure that options hashes don't get messed up by AM:V.
50
- class HitForm < Reform::Form
49
+ class HitForm < TestForm
51
50
  include BandPropertyForm
52
51
  end
53
52
 
54
- class SongForm < Reform::Form
53
+ class SongForm < TestForm
55
54
  include Coercion
56
55
  property :title
57
56
 
58
57
  include BandPropertyForm
59
58
  end
60
59
 
61
-
62
- let (:song) { OpenStruct.new(:band => OpenStruct.new(:title => "Time Again")) }
60
+ let(:song) { OpenStruct.new(band: OpenStruct.new(title: "Time Again")) }
63
61
 
64
62
  # nested form from module is present and creates accessor.
65
- it { SongForm.new(song).band.title.must_equal "Time Again" }
63
+ it { _(SongForm.new(song).band.title).must_equal "Time Again" }
66
64
 
67
65
  # methods from module get included.
68
- it { SongForm.new(song).id.must_equal 1 }
69
- it { SongForm.new(song).band.id.must_equal 2 }
66
+ it { _(SongForm.new(song).id).must_equal 1 }
67
+ it { _(SongForm.new(song).band.id).must_equal 2 }
70
68
 
71
69
  # validators get inherited.
72
70
  it do
73
71
  form = SongForm.new(OpenStruct.new)
74
72
  form.validate({})
75
- form.errors.messages.must_equal({:band=>["is missing"]})
73
+ _(form.errors.messages).must_equal(band: ["must be filled"])
76
74
  end
77
75
 
78
76
  # coercion works
79
77
  it do
80
78
  form = SongForm.new(OpenStruct.new)
81
- form.validate({cool: "1"})
82
- form.cool.must_equal true
79
+ form.validate(cool: "1")
80
+ _(form.cool).must_equal true
83
81
  end
84
82
 
85
-
86
83
  # include a module into a module into a class :)
87
84
  module AlbumFormModule
88
85
  include Reform::Form::Module
@@ -90,29 +87,28 @@ class ModuleInclusionTest < MiniTest::Spec
90
87
 
91
88
  property :name
92
89
  validation do
93
- key(:name).required
90
+ params { required(:name).filled }
94
91
  end
95
92
  end
96
93
 
97
- class AlbumForm < Reform::Form
94
+ class AlbumForm < TestForm
98
95
  include AlbumFormModule
99
96
 
100
97
  # pp heritage
101
- property :band, :inherit => true do
98
+ property :band, inherit: true do
102
99
  property :label
103
100
  validation do
104
- key(:label).required
101
+ params { required(:label).filled }
105
102
  end
106
103
  end
107
104
  end
108
105
 
109
106
  it do
110
- form = AlbumForm.new(OpenStruct.new(:band => OpenStruct.new))
111
- form.validate({"band" => {}})
112
- form.errors.messages.must_equal({:band=>["must be filled"], :"band.title"=>["is missing"], :"band.label"=>["is missing"], :name=>["is missing"]})
107
+ form = AlbumForm.new(OpenStruct.new(band: OpenStruct.new))
108
+ form.validate("band" => {})
109
+ _(form.errors.messages).must_equal("band.title": ["must be filled"], "band.label": ["must be filled"], name: ["must be filled"])
113
110
  end
114
111
 
115
-
116
112
  describe "module with custom accessors" do
117
113
  module SongModule
118
114
  include Reform::Form::Module
@@ -127,16 +123,15 @@ class ModuleInclusionTest < MiniTest::Spec
127
123
  end
128
124
  end
129
125
 
130
- class IncludingSongForm < Reform::Form
126
+ class IncludingSongForm < TestForm
131
127
  include SongModule
132
128
  end
133
129
 
134
- let (:song) { OpenStruct.new(id: 1, title: "Instant Mash") }
130
+ let(:song) { OpenStruct.new(id: 1, title: "Instant Mash") }
135
131
 
136
132
  it do
137
- IncludingSongForm.new(song).id.must_equal 1
138
- IncludingSongForm.new(song).title.must_equal "INSTANT MASH"
133
+ _(IncludingSongForm.new(song).id).must_equal 1
134
+ _(IncludingSongForm.new(song).title).must_equal "INSTANT MASH"
139
135
  end
140
136
  end
141
137
  end
142
-
@@ -0,0 +1,146 @@
1
+ require "test_helper"
2
+ require "reform/form/coercion"
3
+
4
+ class ModuleInclusionTest < MiniTest::Spec
5
+ module BandPropertyForm
6
+ include Reform::Form::Module
7
+
8
+ property :artist
9
+ property :band do
10
+ property :title
11
+
12
+ validation do
13
+ required(:title).filled
14
+ end
15
+
16
+ def id # gets mixed into Form, too.
17
+ 2
18
+ end
19
+ end
20
+
21
+ def id # gets mixed into Form, too.
22
+ 1
23
+ end
24
+
25
+ validation do
26
+ required(:band).filled
27
+ end
28
+
29
+ include Dry::Types.module # allows using Types::* in module.
30
+ property :cool, type: DRY_TYPES_CONSTANT::Bool # test coercion.
31
+
32
+ module InstanceMethods
33
+ def artist=(new_value)
34
+ errors.add(:artist, "this needs to be filled") if new_value.nil?
35
+ super(new_value)
36
+ end
37
+ end
38
+ end
39
+
40
+ # TODO: test if works, move stuff into inherit_schema!
41
+ module AirplaysPropertyForm
42
+ include Reform::Form::Module
43
+
44
+ collection :airplays do
45
+ property :station
46
+ validation do
47
+ required(:station).filled
48
+ end
49
+ end
50
+ validation do
51
+ required(:airplays).filled
52
+ end
53
+ end
54
+
55
+ # test:
56
+ # by including BandPropertyForm into multiple classes we assure that options hashes don't get messed up by AM:V.
57
+ class HitForm < TestForm
58
+ include BandPropertyForm
59
+ end
60
+
61
+ class SongForm < TestForm
62
+ include Coercion
63
+ property :title
64
+
65
+ include BandPropertyForm
66
+ end
67
+
68
+ let(:song) { OpenStruct.new(band: OpenStruct.new(title: "Time Again"), artist: "Ketama") }
69
+
70
+ # nested form from module is present and creates accessor.
71
+ it { _(SongForm.new(song).band.title).must_equal "Time Again" }
72
+ it { _(SongForm.new(song).artist).must_equal "Ketama" }
73
+
74
+ # methods from module get included.
75
+ it { _(SongForm.new(song).id).must_equal 1 }
76
+ it { _(SongForm.new(song).band.id).must_equal 2 }
77
+
78
+ # validators get inherited.
79
+ it do
80
+ form = SongForm.new(OpenStruct.new)
81
+ form.validate(artist: nil)
82
+ _(form.errors.messages).must_equal(artist: ["this needs to be filled"], band: ["must be filled"])
83
+ end
84
+
85
+ # coercion works
86
+ it do
87
+ form = SongForm.new(OpenStruct.new)
88
+ form.validate({cool: "1"})
89
+ _(form.cool).must_equal true
90
+ end
91
+
92
+ # include a module into a module into a class :)
93
+ module AlbumFormModule
94
+ include Reform::Form::Module
95
+ include BandPropertyForm
96
+
97
+ property :name
98
+ validation do
99
+ required(:name).filled
100
+ end
101
+ end
102
+
103
+ class AlbumForm < TestForm
104
+ include AlbumFormModule
105
+
106
+ # pp heritage
107
+ property :band, inherit: true do
108
+ property :label
109
+ validation do
110
+ required(:label).filled
111
+ end
112
+ end
113
+ end
114
+
115
+ it do
116
+ form = AlbumForm.new(OpenStruct.new(band: OpenStruct.new))
117
+ form.validate("band" => {})
118
+ _(form.errors.messages).must_equal("band.title": ["must be filled"], "band.label": ["must be filled"], name: ["must be filled"])
119
+ end
120
+
121
+ describe "module with custom accessors" do
122
+ module SongModule
123
+ include Reform::Form::Module
124
+
125
+ property :id # no custom accessor for id.
126
+ property :title # has custom accessor.
127
+
128
+ module InstanceMethods
129
+ def title
130
+ super.upcase
131
+ end
132
+ end
133
+ end
134
+
135
+ class IncludingSongForm < TestForm
136
+ include SongModule
137
+ end
138
+
139
+ let(:song) { OpenStruct.new(id: 1, title: "Instant Mash") }
140
+
141
+ it do
142
+ _(IncludingSongForm.new(song).id).must_equal 1
143
+ _(IncludingSongForm.new(song).title).must_equal "INSTANT MASH"
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,40 @@
1
+ require "test_helper"
2
+
3
+ class ParseOptionTest < MiniTest::Spec
4
+ Comment = Struct.new(:content, :user)
5
+ User = Struct.new(:name)
6
+
7
+ class CommentForm < TestForm
8
+ property :content
9
+ property :user, parse: false
10
+ end
11
+
12
+ let(:current_user) { User.new("Peter") }
13
+ let(:form) { CommentForm.new(Comment.new, user: current_user) }
14
+
15
+ it do
16
+ _(form.user).must_equal current_user
17
+
18
+ lorem = "Lorem ipsum dolor sit amet..."
19
+ form.validate("content" => lorem, "user" => "not the current user")
20
+
21
+ _(form.content).must_equal lorem
22
+ _(form.user).must_equal current_user
23
+ end
24
+
25
+ describe "using ':parse' option doesn't override other ':deserialize' options" do
26
+ class ArticleCommentForm < TestForm
27
+ property :content
28
+ property :article, deserializer: {instance: "Instance"}
29
+ property :user, parse: false, deserializer: {instance: "Instance"}
30
+ end
31
+
32
+ it do
33
+ _(ArticleCommentForm.definitions.get(:user)[:deserializer][:writeable]).must_equal false
34
+ _(ArticleCommentForm.definitions.get(:user)[:deserializer][:instance]).must_equal "Instance"
35
+
36
+ _(ArticleCommentForm.definitions.get(:article)[:deserializer][:writeable]).must_equal true
37
+ _(ArticleCommentForm.definitions.get(:article)[:deserializer][:instance]).must_equal "Instance"
38
+ end
39
+ end
40
+ end
@@ -3,13 +3,13 @@ require "test_helper"
3
3
  class ParsePipelineTest < MiniTest::Spec
4
4
  Album = Struct.new(:name)
5
5
 
6
- class AlbumForm < Reform::Form
7
- property :name, deserializer: { parse_pipeline: ->(input, options) { Representable::Pipeline[->(input, options) { options[:represented].name = input.inspect }] } }
6
+ class AlbumForm < TestForm
7
+ property :name, deserializer: {parse_pipeline: ->(input, options) { Representable::Pipeline[->(ipt, opts) { opts[:represented].name = ipt.inspect }] }}
8
8
  end
9
9
 
10
10
  it "allows passing :parse_pipeline directly" do
11
11
  form = AlbumForm.new(Album.new)
12
12
  form.validate("name" => "Greatest Hits")
13
- form.name.must_equal "{\"name\"=>\"Greatest Hits\"}"
13
+ _(form.name).must_equal "{\"name\"=>\"Greatest Hits\"}"
14
14
  end
15
- end
15
+ end
@@ -0,0 +1,304 @@
1
+ require "test_helper"
2
+
3
+ class PopulatorTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :songs, :artist)
6
+ Artist = Struct.new(:name)
7
+
8
+ class AlbumForm < TestForm
9
+ property :name, populator: ->(options) { self.name = options[:fragment].reverse }
10
+ validation do
11
+ params { required(:name).filled }
12
+ end
13
+
14
+ collection :songs,
15
+ populator: ->(fragment:, model:, index:, **) {
16
+ (item = model[index]) ? item : model.insert(index, Song.new)
17
+ } do
18
+ property :title
19
+ validation do
20
+ params { required(:title).filled }
21
+ end
22
+
23
+ property :composer, populator: ->(options) { options[:model] || self.composer = Artist.new } do
24
+ property :name
25
+ validation do
26
+ params { required(:name).filled }
27
+ end
28
+ end
29
+ end
30
+
31
+ # property :artist, populator: lambda { |fragment, options| (item = options.binding.get) ? item : Artist.new } do
32
+ # NOTE: we have to document that model here is the twin!
33
+ property :artist, populator: ->(options) { options[:model] || self.artist = Artist.new } do
34
+ property :name
35
+ end
36
+ end
37
+
38
+ let(:song) { Song.new("Broken") }
39
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
40
+ let(:composer) { Artist.new("Greg Graffin") }
41
+ let(:artist) { Artist.new("Bad Religion") }
42
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
43
+
44
+ let(:form) { AlbumForm.new(album) }
45
+
46
+ it "runs populator on scalar" do
47
+ form.validate(
48
+ "name" => "override me!"
49
+ )
50
+
51
+ _(form.name).must_equal "!em edirrevo"
52
+ end
53
+
54
+ # changing existing property :artist.
55
+ # TODO: check with artist==nil
56
+ it do
57
+ old_id = artist.object_id
58
+
59
+ form.validate(
60
+ "artist" => {"name" => "Marcus Miller"}
61
+ )
62
+
63
+ _(form.artist.model.object_id).must_equal old_id
64
+ end
65
+
66
+ # use populator for default value on scalars?
67
+
68
+ # adding to collection via :populator.
69
+ # valid.
70
+ it "yyy" do
71
+ _(form.validate(
72
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
73
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
74
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
75
+ )).must_equal true
76
+
77
+ _(form.errors.messages.inspect).must_equal "{}"
78
+
79
+ # form has updated.
80
+ _(form.name).must_equal "The Dissent Of Man"
81
+ _(form.songs[0].title).must_equal "Fallout"
82
+ _(form.songs[1].title).must_equal "Roxanne"
83
+ _(form.songs[1].composer.name).must_equal "Greg Graffin"
84
+
85
+ _(form.songs[1].composer.model).must_be_instance_of Artist
86
+
87
+ _(form.songs[1].title).must_equal "Roxanne"
88
+ _(form.songs[2].title).must_equal "Rime Of The Ancient Mariner" # new song added.
89
+ _(form.songs[3].title).must_equal "Re-Education"
90
+ _(form.songs[3].composer.name).must_equal "Rise Against"
91
+ _(form.songs.size).must_equal 4
92
+ _(form.artist.name).must_equal "Bad Religion"
93
+
94
+ # model has not changed, yet.
95
+ _(album.name).must_equal "The Dissent Of Man"
96
+ _(album.songs[0].title).must_equal "Broken"
97
+ _(album.songs[1].title).must_equal "Resist Stance"
98
+ _(album.songs[1].composer.name).must_equal "Greg Graffin"
99
+ _(album.songs.size).must_equal 2
100
+ _(album.artist.name).must_equal "Bad Religion"
101
+ end
102
+ end
103
+
104
+ class PopulateWithMethodTest < Minitest::Spec
105
+ Album = Struct.new(:title)
106
+
107
+ class AlbumForm < TestForm
108
+ property :title, populator: :title!
109
+
110
+ def title!(options)
111
+ self.title = options[:fragment].reverse
112
+ end
113
+ end
114
+
115
+ let(:form) { AlbumForm.new(Album.new) }
116
+
117
+ it "runs populator method" do
118
+ form.validate("title" => "override me!")
119
+
120
+ _(form.title).must_equal "!em edirrevo"
121
+ end
122
+ end
123
+
124
+ class PopulateWithCallableTest < Minitest::Spec
125
+ Album = Struct.new(:title)
126
+
127
+ class TitlePopulator
128
+ include Uber::Callable
129
+
130
+ def call(form, options)
131
+ form.title = options[:fragment].reverse
132
+ end
133
+ end
134
+
135
+ class AlbumForm < TestForm
136
+ property :title, populator: TitlePopulator.new
137
+ end
138
+
139
+ let(:form) { AlbumForm.new(Album.new) }
140
+
141
+ it "runs populator method" do
142
+ form.validate("title" => "override me!")
143
+
144
+ _(form.title).must_equal "!em edirrevo"
145
+ end
146
+ end
147
+
148
+ class PopulateWithProcTest < Minitest::Spec
149
+ Album = Struct.new(:title)
150
+
151
+ TitlePopulator = ->(options) do
152
+ options[:represented].title = options[:fragment].reverse
153
+ end
154
+
155
+ class AlbumForm < TestForm
156
+ property :title, populator: TitlePopulator
157
+ end
158
+
159
+ let(:form) { AlbumForm.new(Album.new) }
160
+
161
+ it "runs populator method" do
162
+ form.validate("title" => "override me!")
163
+
164
+ _(form.title).must_equal "!em edirrevo"
165
+ end
166
+ end
167
+
168
+ class PopulateIfEmptyTest < MiniTest::Spec
169
+ Song = Struct.new(:title, :album, :composer)
170
+ Album = Struct.new(:name, :songs, :artist)
171
+ Artist = Struct.new(:name)
172
+
173
+ let(:song) { Song.new("Broken") }
174
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
175
+ let(:composer) { Artist.new("Greg Graffin") }
176
+ let(:artist) { Artist.new("Bad Religion") }
177
+ let(:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
178
+
179
+ class AlbumForm < TestForm
180
+ property :name
181
+
182
+ collection :songs,
183
+ populate_if_empty: Song do # class name works.
184
+
185
+ property :title
186
+ validation do
187
+ params { required(:title).filled }
188
+ end
189
+
190
+ property :composer, populate_if_empty: :populate_composer! do # lambda works, too. in form context.
191
+ property :name
192
+ validation do
193
+ params { required(:name).filled }
194
+ end
195
+ end
196
+
197
+ private
198
+ def populate_composer!(options)
199
+ Artist.new
200
+ end
201
+ end
202
+
203
+ property :artist, populate_if_empty: ->(args) { create_artist(args[:fragment], args[:user_options]) } do # methods work, too.
204
+ property :name
205
+ end
206
+
207
+ private
208
+ class Sting < Artist
209
+ attr_accessor :args
210
+ end
211
+ def create_artist(input, user_options)
212
+ Sting.new.tap { |artist| artist.args = ([input, user_options].to_s) }
213
+ end
214
+ end
215
+
216
+ let(:form) { AlbumForm.new(album) }
217
+
218
+ it do
219
+ _(form.songs.size).must_equal 2
220
+
221
+ _(form.validate(
222
+ "songs" => [{"title" => "Fallout"}, {"title" => "Roxanne"},
223
+ {"title" => "Rime Of The Ancient Mariner"}, # new song.
224
+ {"title" => "Re-Education", "composer" => {"name" => "Rise Against"}}], # new song with new composer.
225
+ )).must_equal true
226
+
227
+ _(form.errors.messages.inspect).must_equal "{}"
228
+
229
+ # form has updated.
230
+ _(form.name).must_equal "The Dissent Of Man"
231
+ _(form.songs[0].title).must_equal "Fallout"
232
+ _(form.songs[1].title).must_equal "Roxanne"
233
+ _(form.songs[1].composer.name).must_equal "Greg Graffin"
234
+ _(form.songs[1].title).must_equal "Roxanne"
235
+ _(form.songs[2].title).must_equal "Rime Of The Ancient Mariner" # new song added.
236
+ _(form.songs[3].title).must_equal "Re-Education"
237
+ _(form.songs[3].composer.name).must_equal "Rise Against"
238
+ _(form.songs.size).must_equal 4
239
+ _(form.artist.name).must_equal "Bad Religion"
240
+
241
+ # model has not changed, yet.
242
+ _(album.name).must_equal "The Dissent Of Man"
243
+ _(album.songs[0].title).must_equal "Broken"
244
+ _(album.songs[1].title).must_equal "Resist Stance"
245
+ _(album.songs[1].composer.name).must_equal "Greg Graffin"
246
+ _(album.songs.size).must_equal 2
247
+ _(album.artist.name).must_equal "Bad Religion"
248
+ end
249
+
250
+ # trigger artist populator. lambda calling form instance method.
251
+ it "xxxx" do
252
+ form = AlbumForm.new(album = Album.new)
253
+ form.validate("artist" => {"name" => "From Autumn To Ashes"})
254
+
255
+ _(form.artist.name).must_equal "From Autumn To Ashes"
256
+ # test lambda was executed in form context.
257
+ _(form.artist.model).must_be_instance_of AlbumForm::Sting
258
+ # test lambda block arguments.
259
+ _(form.artist.model.args.to_s).must_equal "[{\"name\"=>\"From Autumn To Ashes\"}, nil]"
260
+
261
+ assert_nil album.artist
262
+ end
263
+ end
264
+
265
+ # delete songs while deserializing.
266
+ class PopulateIfEmptyWithDeletionTest < MiniTest::Spec
267
+ Song = Struct.new(:title, :album, :composer)
268
+ Album = Struct.new(:name, :songs, :artist)
269
+
270
+ let(:song) { Song.new("Broken") }
271
+ let(:song2) { Song.new("Resist Stance") }
272
+ let(:album) { Album.new("The Dissent Of Man", [song, song2]) }
273
+
274
+ class AlbumForm < TestForm
275
+ property :name
276
+
277
+ collection :songs,
278
+ populate_if_empty: Song, skip_if: :delete_song! do
279
+
280
+ property :title
281
+ validation do
282
+ params { required(:title).filled }
283
+ end
284
+ end
285
+
286
+ def delete_song!(options)
287
+ songs.delete(songs[0]) and return true if options[:fragment]["title"] == "Broken, delete me!"
288
+ false
289
+ end
290
+ end
291
+
292
+ let(:form) { AlbumForm.new(album) }
293
+
294
+ it do
295
+ _(form.validate(
296
+ "songs" => [{"title" => "Broken, delete me!"}, {"title" => "Roxanne"}]
297
+ )).must_equal true
298
+
299
+ _(form.errors.messages.inspect).must_equal "{}"
300
+
301
+ _(form.songs.size).must_equal 1
302
+ _(form.songs[0].title).must_equal "Roxanne"
303
+ end
304
+ end