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,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
+ 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
@@ -1,12 +1,12 @@
1
- require 'test_helper'
2
- require 'reform/form/coercion'
1
+ require "test_helper"
2
+ require "reform/form/coercion"
3
3
 
4
4
  class ChangedTest < MiniTest::Spec
5
5
  Song = Struct.new(:title, :album, :composer)
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
@@ -18,24 +18,24 @@ class ChangedTest < MiniTest::Spec
18
18
  end
19
19
  end
20
20
 
21
- let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
22
- let (:composer) { Artist.new("Greg Graffin") }
23
- let (:album) { Album.new("The Dissent Of Man", [song_with_composer]) }
21
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
22
+ let(:composer) { Artist.new("Greg Graffin") }
23
+ let(:album) { Album.new("The Dissent Of Man", [song_with_composer]) }
24
24
 
25
- let (:form) { AlbumForm.new(album) }
25
+ let(:form) { AlbumForm.new(album) }
26
26
 
27
27
  # nothing changed after setup.
28
28
  it do
29
- form.changed?(:name).must_equal false
30
- form.songs[0].changed?(:title).must_equal false
31
- form.songs[0].composer.changed?(:name).must_equal false
29
+ _(form.changed?(:name)).must_equal false
30
+ _(form.songs[0].changed?(:title)).must_equal false
31
+ _(form.songs[0].composer.changed?(:name)).must_equal false
32
32
  end
33
33
 
34
34
  # after validate, things might have changed.
35
35
  it do
36
36
  form.validate("name" => "Out Of Bounds", "songs" => [{"composer" => {"name" => "Ingemar Jansson & Mikael Danielsson"}}])
37
- form.changed?(:name).must_equal true
38
- form.songs[0].changed?(:title).must_equal false
39
- form.songs[0].composer.changed?(:name).must_equal true
37
+ _(form.changed?(:name)).must_equal true
38
+ _(form.songs[0].changed?(:title)).must_equal false
39
+ _(form.songs[0].composer.changed?(:name)).must_equal true
40
40
  end
41
- end
41
+ end
@@ -1,21 +1,23 @@
1
1
  require "test_helper"
2
2
  require "reform/form/coercion"
3
+ require "disposable/twin/property/hash"
3
4
 
4
5
  class CoercionTest < BaseTest
5
6
  class Irreversible
6
7
  def self.call(value)
7
- value*2
8
+ value * 2
8
9
  end
9
10
  end
10
11
 
11
- class Form < Reform::Form
12
+ class Form < TestForm
12
13
  feature Coercion
14
+ include Disposable::Twin::Property::Hash
13
15
 
14
- property :released_at, type: Types::Form::DateTime
16
+ property :released_at, type: DRY_TYPES_CONSTANT::DateTime
15
17
 
16
18
  property :hit do
17
- property :length, type: Types::Form::Int
18
- property :good, type: Types::Form::Bool
19
+ property :length, type: DRY_TYPES_INT_CONSTANT
20
+ property :good, type: DRY_TYPES_CONSTANT::Bool
19
21
  end
20
22
 
21
23
  property :band do
@@ -23,44 +25,74 @@ class CoercionTest < BaseTest
23
25
  property :value, type: Irreversible
24
26
  end
25
27
  end
28
+
29
+ property :metadata, field: :hash do
30
+ property :publication_settings do
31
+ property :featured, type: DRY_TYPES_CONSTANT::Bool
32
+ end
33
+ end
26
34
  end
27
35
 
28
36
  subject do
29
37
  Form.new(album)
30
38
  end
31
39
 
32
- let (:album) {
40
+ let(:album) do
33
41
  OpenStruct.new(
34
- :released_at => "31/03/1981",
35
- :hit => OpenStruct.new(:length => "312"),
36
- :band => Band.new(OpenStruct.new(:value => "9999.99"))
42
+ released_at: "31/03/1981",
43
+ hit: OpenStruct.new(length: "312"),
44
+ band: Band.new(OpenStruct.new(value: "9999.99")),
45
+ metadata: {}
37
46
  )
38
- }
47
+ end
39
48
 
40
49
  # it { subject.released_at.must_be_kind_of DateTime }
41
- it { subject.released_at.must_equal "31/03/1981" } # NO coercion in setup.
42
- it { subject.hit.length.must_equal "312" }
43
- it { subject.band.label.value.must_equal "9999.99" }
44
-
50
+ it { _(subject.released_at).must_equal "31/03/1981" } # NO coercion in setup.
51
+ it { _(subject.hit.length).must_equal "312" }
52
+ it { _(subject.band.label.value).must_equal "9999.99" }
45
53
 
46
- let (:params) {
54
+ let(:params) do
47
55
  {
48
- :released_at => "30/03/1981",
49
- :hit => {:length => "312"},
50
- :band => {:label => {:value => "9999.99"}}
56
+ released_at: "30/03/1981",
57
+ hit: {
58
+ length: "312",
59
+ good: "0",
60
+ },
61
+ band: {
62
+ label: {
63
+ value: "9999.99"
64
+ }
65
+ },
66
+ metadata: {
67
+ publication_settings: {
68
+ featured: "0"
69
+ }
70
+ }
51
71
  }
52
- }
53
-
72
+ end
54
73
 
55
74
  # validate
56
75
  describe "#validate" do
57
76
  before { subject.validate(params) }
58
77
 
59
- it { subject.released_at.must_equal DateTime.parse("30/03/1981") }
60
- it { subject.hit.length.must_equal 312 }
61
- it { subject.hit.good.must_equal nil }
62
- it { subject.band.label.value.must_equal "9999.999999.99" } # coercion happened once.
78
+ it { _(subject.released_at).must_equal DateTime.parse("30/03/1981") }
79
+ it { _(subject.hit.length).must_equal 312 }
80
+ it { _(subject.hit.good).must_equal false }
81
+ it { _(subject.band.label.value).must_equal "9999.999999.99" } # coercion happened once.
82
+ it { _(subject.metadata.publication_settings.featured).must_equal false }
63
83
  end
64
84
 
65
- # save
85
+ # sync
86
+ describe "#sync" do
87
+ before do
88
+ _(subject.validate(params)).must_equal true
89
+ subject.sync
90
+ end
91
+
92
+ it { _(album.released_at).must_equal DateTime.parse("30/03/1981") }
93
+ it { _(album.hit.length).must_equal 312 }
94
+ it { _(album.hit.good).must_equal false }
95
+ it { assert_nil album.metadata[:publication_settings] }
96
+ it { _(album.metadata["publication_settings"]["featured"]).must_equal false }
97
+ end
66
98
  end
@@ -0,0 +1,186 @@
1
+ require "test_helper"
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
+ let(:measurement) { Measurement.new(:l) }
30
+ let(:tshirt) { Tshirt.new(2, :m) }
31
+ let(:form) { OutfitForm.new(tshirt: tshirt, measurement: measurement) }
32
+
33
+ Tshirt = Struct.new(:price, :size)
34
+ Measurement = Struct.new(:size)
35
+
36
+ it { _(form.price).must_equal 6 }
37
+ it { _(form.price(for_size: :s)).must_equal 2 }
38
+ end
39
+
40
+ class FormCompositionTest < MiniTest::Spec
41
+ Song = Struct.new(:id, :title, :band)
42
+ Requester = Struct.new(:id, :name, :requester)
43
+ Band = Struct.new(:title)
44
+
45
+ class RequestForm < TestForm
46
+ include Composition
47
+
48
+ property :name, on: :requester
49
+ property :requester_id, on: :requester, from: :id
50
+ properties :title, :id, on: :song
51
+ # property :channel # FIXME: what about the "main model"?
52
+ property :channel, virtual: true, on: :song
53
+ property :requester, on: :requester
54
+ property :captcha, on: :song, virtual: true
55
+
56
+ validation do
57
+ params do
58
+ required(:name).filled
59
+ required(:title).filled
60
+ end
61
+ end
62
+
63
+ property :band, on: :song do
64
+ property :title
65
+ end
66
+ end
67
+
68
+ let(:form) { RequestForm.new(song: song, requester: requester) }
69
+ let(:song) { Song.new(1, "Rio", band) }
70
+ let(:requester) { Requester.new(2, "Duran Duran", "MCP") }
71
+ let(:band) { Band.new("Duran^2") }
72
+
73
+ # delegation form -> composition works
74
+ it { _(form.id).must_equal 1 }
75
+ it { _(form.title).must_equal "Rio" }
76
+ it { _(form.name).must_equal "Duran Duran" }
77
+ it { _(form.requester_id).must_equal 2 }
78
+ it { assert_nil form.channel }
79
+ it { _(form.requester).must_equal "MCP" } # same name as composed model.
80
+ it { assert_nil form.captcha }
81
+
82
+ # #model just returns <Composition>.
83
+ it { _(form.mapper).must_be_kind_of Disposable::Composition }
84
+
85
+ # #model[] -> composed models
86
+ it { _(form.model[:requester]).must_equal requester }
87
+ it { _(form.model[:song]).must_equal song }
88
+
89
+ it "creates Composition for you" do
90
+ _(form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb")).must_equal true
91
+ _(form.validate("title" => "", "name" => "Frenzal Rhomb")).must_equal false
92
+ end
93
+
94
+ describe "#save" do
95
+ # #save with {}
96
+ it do
97
+ hash = {}
98
+
99
+ form.save do |map|
100
+ hash[:name] = form.name
101
+ hash[:title] = form.title
102
+ end
103
+
104
+ _(hash).must_equal({name: "Duran Duran", title: "Rio"})
105
+ end
106
+
107
+ it "provides nested symbolized hash as second block argument" do
108
+ form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "channel" => "JJJ", "captcha" => "wonderful")
109
+
110
+ hash = nil
111
+
112
+ form.save do |map|
113
+ hash = map
114
+ end
115
+
116
+ _(hash).must_equal({
117
+ song: {"title" => "Greyhound", "id" => 1, "channel" => "JJJ", "captcha" => "wonderful", "band" => {"title" => "Duran^2"}},
118
+ requester: {"name" => "Frenzal Rhomb", "id" => 2, "requester" => "MCP"}
119
+ }
120
+ )
121
+ end
122
+
123
+ it "xxx pushes data to models and calls #save when no block passed" do
124
+ song.extend(Saveable)
125
+ requester.extend(Saveable)
126
+ band.extend(Saveable)
127
+
128
+ form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
129
+ _(form.captcha).must_equal "1337" # TODO: move to separate test.
130
+
131
+ form.save
132
+
133
+ _(requester.name).must_equal "Frenzal Rhomb"
134
+ _(requester.saved?).must_equal true
135
+ _(song.title).must_equal "Greyhound"
136
+ _(song.saved?).must_equal true
137
+ _(song.band.title).must_equal "Duran^2"
138
+ _(song.band.saved?).must_equal true
139
+ end
140
+
141
+ it "returns true when models all save successfully" do
142
+ song.extend(Saveable)
143
+ requester.extend(Saveable)
144
+ band.extend(Saveable)
145
+
146
+ _(form.save).must_equal true
147
+ end
148
+
149
+ it "returns false when one or more models don't save successfully" do
150
+ module Unsaveable
151
+ def save
152
+ false
153
+ end
154
+ end
155
+
156
+ song.extend(Unsaveable)
157
+ requester.extend(Saveable)
158
+ band.extend(Saveable)
159
+
160
+ _(form.save).must_equal false
161
+ end
162
+ end
163
+ end
164
+
165
+ class FormCompositionCollectionTest < MiniTest::Spec
166
+ Book = Struct.new(:id, :name)
167
+ Library = Struct.new(:id) do
168
+ def books
169
+ [Book.new(1, "My book")]
170
+ end
171
+ end
172
+
173
+ class LibraryForm < TestForm
174
+ include Reform::Form::Composition
175
+
176
+ collection :books, on: :library do
177
+ property :id
178
+ property :name
179
+ end
180
+ end
181
+
182
+ let(:form) { LibraryForm.new(library: library) }
183
+ let(:library) { Library.new(2) }
184
+
185
+ it { form.save { |hash| _(hash).must_equal({library: {"books" => [{"id" => 1, "name" => "My book"}]}}) } }
186
+ end
@@ -0,0 +1,184 @@
1
+ require "test_helper"
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
+ let(:measurement) { Measurement.new(:l) }
30
+ let(:tshirt) { Tshirt.new(2, :m) }
31
+ let(:form) { OutfitForm.new(tshirt: tshirt, measurement: measurement) }
32
+
33
+ Tshirt = Struct.new(:price, :size)
34
+ Measurement = Struct.new(:size)
35
+
36
+ it { _(form.price).must_equal 6 }
37
+ it { _(form.price(for_size: :s)).must_equal 2 }
38
+ end
39
+
40
+ class FormCompositionTest < MiniTest::Spec
41
+ Song = Struct.new(:id, :title, :band)
42
+ Requester = Struct.new(:id, :name, :requester)
43
+ Band = Struct.new(:title)
44
+
45
+ class RequestForm < TestForm
46
+ include Composition
47
+
48
+ property :name, on: :requester
49
+ property :requester_id, on: :requester, from: :id
50
+ properties :title, :id, on: :song
51
+ # property :channel # FIXME: what about the "main model"?
52
+ property :channel, virtual: true, on: :song
53
+ property :requester, on: :requester
54
+ property :captcha, on: :song, virtual: true
55
+
56
+ validation do
57
+ required(:name).filled
58
+ required(:title).filled
59
+ end
60
+
61
+ property :band, on: :song do
62
+ property :title
63
+ end
64
+ end
65
+
66
+ let(:form) { RequestForm.new(song: song, requester: requester) }
67
+ let(:song) { Song.new(1, "Rio", band) }
68
+ let(:requester) { Requester.new(2, "Duran Duran", "MCP") }
69
+ let(:band) { Band.new("Duran^2") }
70
+
71
+ # delegation form -> composition works
72
+ it { _(form.id).must_equal 1 }
73
+ it { _(form.title).must_equal "Rio" }
74
+ it { _(form.name).must_equal "Duran Duran" }
75
+ it { _(form.requester_id).must_equal 2 }
76
+ it { assert_nil form.channel }
77
+ it { _(form.requester).must_equal "MCP" } # same name as composed model.
78
+ it { assert_nil form.captcha }
79
+
80
+ # #model just returns <Composition>.
81
+ it { _(form.mapper).must_be_kind_of Disposable::Composition }
82
+
83
+ # #model[] -> composed models
84
+ it { _(form.model[:requester]).must_equal requester }
85
+ it { _(form.model[:song]).must_equal song }
86
+
87
+ it "creates Composition for you" do
88
+ _(form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb")).must_equal true
89
+ _(form.validate("title" => "", "name" => "Frenzal Rhomb")).must_equal false
90
+ end
91
+
92
+ describe "#save" do
93
+ # #save with {}
94
+ it do
95
+ hash = {}
96
+
97
+ form.save do |map|
98
+ hash[:name] = form.name
99
+ hash[:title] = form.title
100
+ end
101
+
102
+ _(hash).must_equal({name: "Duran Duran", title: "Rio"})
103
+ end
104
+
105
+ it "provides nested symbolized hash as second block argument" do
106
+ form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "channel" => "JJJ", "captcha" => "wonderful")
107
+
108
+ hash = nil
109
+
110
+ form.save do |map|
111
+ hash = map
112
+ end
113
+
114
+ _(hash).must_equal({
115
+ song: {"title" => "Greyhound", "id" => 1, "channel" => "JJJ", "captcha" => "wonderful", "band" => {"title" => "Duran^2"}},
116
+ requester: {"name" => "Frenzal Rhomb", "id" => 2, "requester" => "MCP"}
117
+ }
118
+ )
119
+ end
120
+
121
+ it "xxx pushes data to models and calls #save when no block passed" do
122
+ song.extend(Saveable)
123
+ requester.extend(Saveable)
124
+ band.extend(Saveable)
125
+
126
+ form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
127
+ _(form.captcha).must_equal "1337" # TODO: move to separate test.
128
+
129
+ form.save
130
+
131
+ _(requester.name).must_equal "Frenzal Rhomb"
132
+ _(requester.saved?).must_equal true
133
+ _(song.title).must_equal "Greyhound"
134
+ _(song.saved?).must_equal true
135
+ _(song.band.title).must_equal "Duran^2"
136
+ _(song.band.saved?).must_equal true
137
+ end
138
+
139
+ it "returns true when models all save successfully" do
140
+ song.extend(Saveable)
141
+ requester.extend(Saveable)
142
+ band.extend(Saveable)
143
+
144
+ _(form.save).must_equal true
145
+ end
146
+
147
+ it "returns false when one or more models don't save successfully" do
148
+ module Unsaveable
149
+ def save
150
+ false
151
+ end
152
+ end
153
+
154
+ song.extend(Unsaveable)
155
+ requester.extend(Saveable)
156
+ band.extend(Saveable)
157
+
158
+ _(form.save).must_equal false
159
+ end
160
+ end
161
+ end
162
+
163
+ class FormCompositionCollectionTest < MiniTest::Spec
164
+ Book = Struct.new(:id, :name)
165
+ Library = Struct.new(:id) do
166
+ def books
167
+ [Book.new(1, "My book")]
168
+ end
169
+ end
170
+
171
+ class LibraryForm < TestForm
172
+ include Reform::Form::Composition
173
+
174
+ collection :books, on: :library do
175
+ property :id
176
+ property :name
177
+ end
178
+ end
179
+
180
+ let(:form) { LibraryForm.new(library: library) }
181
+ let(:library) { Library.new(2) }
182
+
183
+ it { form.save { |hash| _(hash).must_equal({library: {"books" => [{"id" => 1, "name" => "My book"}]}}) } }
184
+ end