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,55 @@
1
+ require "test_helper"
2
+
3
+ class CustomerErrorTest < MiniTest::Spec
4
+ let(:key) { :name }
5
+ let(:error_text) { "text2" }
6
+ let(:starting_error) { [OpenStruct.new(errors: {title: ["text1"]})] }
7
+
8
+ let(:custom_error) { Reform::Contract::CustomError.new(key, error_text, @results) }
9
+
10
+ before { @results = starting_error }
11
+
12
+ it "base class structure" do
13
+ assert_equal custom_error.success?, false
14
+ assert_equal custom_error.failure?, true
15
+ assert_equal custom_error.errors, key => [error_text]
16
+ assert_equal custom_error.messages, key => [error_text]
17
+ assert_equal custom_error.hint, {}
18
+ end
19
+
20
+ describe "updates @results accordingly" do
21
+ it "add new key" do
22
+ custom_error
23
+
24
+ assert_equal @results.size, 2
25
+ errors = @results.map(&:errors)
26
+
27
+ assert_equal errors[0], starting_error.first.errors
28
+ assert_equal errors[1], custom_error.errors
29
+ end
30
+
31
+ describe "when key error already exists in @results" do
32
+ let(:key) { :title }
33
+
34
+ it "merge errors text" do
35
+ custom_error
36
+
37
+ assert_equal @results.size, 1
38
+
39
+ assert_equal @results.first.errors.values, [%w[text1 text2]]
40
+ end
41
+
42
+ describe "add error text is already" do
43
+ let(:error_text) { "text1" }
44
+
45
+ it 'does not create duplicates' do
46
+ custom_error
47
+
48
+ assert_equal @results.size, 1
49
+
50
+ assert_equal @results.first.errors.values, [%w[text1]]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,77 @@
1
+ require "test_helper"
2
+
3
+ class ContractTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :duration, :songs, :artist)
6
+ Artist = Struct.new(:name)
7
+
8
+ class ArtistForm < TestForm
9
+ property :name
10
+ end
11
+
12
+ class AlbumForm < TestContract
13
+ property :name
14
+
15
+ properties :duration
16
+ properties :year, :style, readable: false
17
+
18
+ validation do
19
+ params { required(:name).filled }
20
+ end
21
+
22
+ collection :songs do
23
+ property :title
24
+ validation do
25
+ params { required(:title).filled }
26
+ end
27
+
28
+ property :composer do
29
+ property :name
30
+ validation do
31
+ params { required(:name).filled }
32
+ end
33
+ end
34
+ end
35
+
36
+ property :artist, form: ArtistForm
37
+ end
38
+
39
+ let(:song) { Song.new("Broken") }
40
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
41
+ let(:composer) { Artist.new("Greg Graffin") }
42
+ let(:artist) { Artist.new("Bad Religion") }
43
+ let(:album) { Album.new("The Dissent Of Man", 123, [song, song_with_composer], artist) }
44
+
45
+ let(:form) { AlbumForm.new(album) }
46
+
47
+ # accept `property form: SongForm`.
48
+ it do
49
+ _(form.artist).must_be_instance_of ArtistForm
50
+ end
51
+
52
+ describe ".properties" do
53
+ it "defines a property when called with one argument" do
54
+ _(form).must_respond_to :duration
55
+ end
56
+
57
+ it "defines several properties when called with multiple arguments" do
58
+ _(form).must_respond_to :year
59
+ _(form).must_respond_to :style
60
+ end
61
+
62
+ it "passes options to each property when options are provided" do
63
+ readable = AlbumForm.new(album).options_for(:style)[:readable]
64
+ _(readable).must_equal false
65
+ end
66
+
67
+ it "returns the list of defined properties" do
68
+ returned_value = AlbumForm.properties(:hello, :world, virtual: true)
69
+ _(returned_value).must_equal %i[hello world]
70
+ end
71
+ end
72
+
73
+ describe "#options_for" do
74
+ it { _(AlbumForm.options_for(:name).extend(Declarative::Inspect).inspect).must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
75
+ it { _(AlbumForm.new(album).options_for(:name).extend(Declarative::Inspect).inspect).must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
76
+ end
77
+ end
@@ -0,0 +1,77 @@
1
+ require "test_helper"
2
+
3
+ class ContractTest < MiniTest::Spec
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :duration, :songs, :artist)
6
+ Artist = Struct.new(:name)
7
+
8
+ class ArtistForm < TestForm
9
+ property :name
10
+ end
11
+
12
+ class AlbumForm < TestContract
13
+ property :name
14
+
15
+ properties :duration
16
+ properties :year, :style, readable: false
17
+
18
+ validation do
19
+ required(:name).filled
20
+ end
21
+
22
+ collection :songs do
23
+ property :title
24
+ validation do
25
+ required(:title).filled
26
+ end
27
+
28
+ property :composer do
29
+ property :name
30
+ validation do
31
+ required(:name).filled
32
+ end
33
+ end
34
+ end
35
+
36
+ property :artist, form: ArtistForm
37
+ end
38
+
39
+ let(:song) { Song.new("Broken") }
40
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
41
+ let(:composer) { Artist.new("Greg Graffin") }
42
+ let(:artist) { Artist.new("Bad Religion") }
43
+ let(:album) { Album.new("The Dissent Of Man", 123, [song, song_with_composer], artist) }
44
+
45
+ let(:form) { AlbumForm.new(album) }
46
+
47
+ # accept `property form: SongForm`.
48
+ it do
49
+ _(form.artist).must_be_instance_of ArtistForm
50
+ end
51
+
52
+ describe ".properties" do
53
+ it "defines a property when called with one argument" do
54
+ _(form).must_respond_to :duration
55
+ end
56
+
57
+ it "defines several properties when called with multiple arguments" do
58
+ _(form).must_respond_to :year
59
+ _(form).must_respond_to :style
60
+ end
61
+
62
+ it "passes options to each property when options are provided" do
63
+ readable = AlbumForm.new(album).options_for(:style)[:readable]
64
+ _(readable).must_equal false
65
+ end
66
+
67
+ it "returns the list of defined properties" do
68
+ returned_value = AlbumForm.properties(:hello, :world, virtual: true)
69
+ _(returned_value).must_equal %i[hello world]
70
+ end
71
+ end
72
+
73
+ describe "#options_for" do
74
+ it { _(AlbumForm.options_for(:name).extend(Declarative::Inspect).inspect).must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
75
+ it { _(AlbumForm.new(album).options_for(:name).extend(Declarative::Inspect).inspect).must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
76
+ end
77
+ end
@@ -5,7 +5,7 @@ class DefaultTest < Minitest::Spec
5
5
  Album = Struct.new(:name, :songs, :artist)
6
6
  Artist = Struct.new(:name)
7
7
 
8
- class AlbumForm < Reform::Form
8
+ class AlbumForm < TestForm
9
9
  property :name, default: "Wrong"
10
10
 
11
11
  collection :songs do
@@ -16,7 +16,7 @@ class DefaultTest < Minitest::Spec
16
16
  it do
17
17
  form = AlbumForm.new(Album.new(nil, [Song.new]))
18
18
 
19
- form.name.must_equal "Wrong"
20
- form.songs[0].title.must_equal "It's Catching Up"
19
+ _(form.name).must_equal "Wrong"
20
+ _(form.songs[0].title).must_equal "It's Catching Up"
21
21
  end
22
- end
22
+ end
@@ -1,4 +1,4 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
  require "representable/json"
3
3
 
4
4
  class DeserializeTest < MiniTest::Spec
@@ -6,11 +6,11 @@ class DeserializeTest < MiniTest::Spec
6
6
  Album = Struct.new(:title, :artist)
7
7
  Artist = Struct.new(:name, :callname)
8
8
 
9
- class JsonAlbumForm < Reform::Form
9
+ class JsonAlbumForm < TestForm
10
10
  module Json
11
11
  def deserialize(params)
12
12
  deserializer.new(self).
13
- # extend(Representable::Debug).
13
+ # extend(Representable::Debug).
14
14
  from_json(params)
15
15
  end
16
16
 
@@ -18,7 +18,7 @@ class DeserializeTest < MiniTest::Spec
18
18
  Disposable::Rescheme.from(self.class,
19
19
  include: [Representable::JSON],
20
20
  superclass: Representable::Decorator,
21
- definitions_from: lambda { |inline| inline.definitions },
21
+ definitions_from: ->(inline) { inline.definitions },
22
22
  options_from: :deserializer,
23
23
  exclude_options: [:populator]
24
24
  )
@@ -26,15 +26,13 @@ class DeserializeTest < MiniTest::Spec
26
26
  end
27
27
  include Json
28
28
 
29
-
30
29
  property :title
31
30
  property :artist, populate_if_empty: Artist do
32
31
  property :name
33
32
  end
34
33
  end
35
34
 
36
-
37
- let (:artist) { Artist.new("A-ha") }
35
+ let(:artist) { Artist.new("A-ha") }
38
36
  it do
39
37
  artist_id = artist.object_id
40
38
 
@@ -43,13 +41,13 @@ class DeserializeTest < MiniTest::Spec
43
41
 
44
42
  form.validate(json)
45
43
 
46
- form.title.must_equal "Apocalypse Soon"
47
- form.artist.name.must_equal "Mute"
48
- form.artist.model.object_id.must_equal artist_id
44
+ _(form.title).must_equal "Apocalypse Soon"
45
+ _(form.artist.name).must_equal "Mute"
46
+ _(form.artist.model.object_id).must_equal artist_id
49
47
  end
50
48
 
51
49
  describe "infering the deserializer from another form should NOT copy its populators" do
52
- class CompilationForm < Reform::Form
50
+ class CompilationForm < TestForm
53
51
  property :artist, populator: ->(options) { self.artist = Artist.new(nil, options[:fragment].to_s) } do
54
52
  property :name
55
53
  end
@@ -62,20 +60,19 @@ class DeserializeTest < MiniTest::Spec
62
60
  # also tests the Form#deserializer API. # FIXME.
63
61
  it "uses deserializer inferred from JsonAlbumForm but deserializes/populates to CompilationForm" do
64
62
  form = CompilationForm.new(Album.new)
65
- form.validate("artist"=> {"name" => "Horowitz"}) # the deserializer doesn't know symbols.
63
+ form.validate("artist" => {"name" => "Horowitz"}) # the deserializer doesn't know symbols.
66
64
  form.sync
67
- form.artist.model.must_equal Artist.new("Horowitz", %{{"name"=>"Horowitz"}})
65
+ _(form.artist.model).must_equal Artist.new("Horowitz", %{{"name"=>"Horowitz"}})
68
66
  end
69
67
  end
70
68
  end
71
69
 
72
-
73
70
  class ValidateWithBlockTest < MiniTest::Spec
74
71
  Song = Struct.new(:title, :album, :composer)
75
72
  Album = Struct.new(:title, :artist)
76
73
  Artist = Struct.new(:name)
77
74
 
78
- class AlbumForm < Reform::Form
75
+ class AlbumForm < TestForm
79
76
  property :title
80
77
  property :artist, populate_if_empty: Artist do
81
78
  property :name
@@ -90,15 +87,15 @@ class ValidateWithBlockTest < MiniTest::Spec
90
87
  deserializer = Disposable::Rescheme.from(AlbumForm,
91
88
  include: [Representable::JSON],
92
89
  superclass: Representable::Decorator,
93
- definitions_from: lambda { |inline| inline.definitions },
90
+ definitions_from: ->(inline) { inline.definitions },
94
91
  options_from: :deserializer
95
92
  )
96
93
 
97
- form.validate(json) do |params|
94
+ _(form.validate(json) { |params|
98
95
  deserializer.new(form).from_json(params)
99
- end.must_equal true # with block must return result, too.
96
+ }).must_equal true # with block must return result, too.
100
97
 
101
- form.title.must_equal "Apocalypse Soon"
102
- form.artist.name.must_equal "Mute"
98
+ _(form.title).must_equal "Apocalypse Soon"
99
+ _(form.artist.name).must_equal "Mute"
103
100
  end
104
101
  end
@@ -0,0 +1,225 @@
1
+ require "test_helper"
2
+
3
+ class ErrorsTest < MiniTest::Spec
4
+ class AlbumForm < TestForm
5
+ property :title
6
+ validation do
7
+ params { required(:title).filled }
8
+ end
9
+
10
+ property :artists, default: []
11
+ property :producer do
12
+ property :name
13
+ end
14
+
15
+ property :hit do
16
+ property :title
17
+ validation do
18
+ params { required(:title).filled }
19
+ end
20
+ end
21
+
22
+ collection :songs do
23
+ property :title
24
+ validation do
25
+ params { required(:title).filled }
26
+ end
27
+ end
28
+
29
+ property :band do # yepp, people do crazy stuff like that.
30
+ property :name
31
+ property :label do
32
+ property :name
33
+ validation do
34
+ params { required(:name).filled }
35
+ end
36
+ end
37
+ # TODO: make band a required object.
38
+
39
+ validation do
40
+ config.messages.load_paths << "test/fixtures/dry_new_api_error_messages.yml"
41
+
42
+ params { required(:name).filled }
43
+
44
+ rule(:name) { key.failure(:good_musical_taste?) if value == "Nickelback" }
45
+ end
46
+ end
47
+
48
+ validation do
49
+ params do
50
+ required(:title).filled
51
+ required(:artists).each(:str?)
52
+ required(:producer).hash do
53
+ required(:name).filled
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ let(:album_title) { "Blackhawks Over Los Angeles" }
60
+ let(:album) do
61
+ OpenStruct.new(
62
+ title: album_title,
63
+ hit: song,
64
+ songs: songs, # TODO: document this requirement,
65
+ band: Struct.new(:name, :label).new("Epitaph", OpenStruct.new),
66
+ producer: Struct.new(:name).new("Sun Records")
67
+ )
68
+ end
69
+ let(:song) { OpenStruct.new(title: "Downtown") }
70
+ let(:songs) { [song = OpenStruct.new(title: "Calling"), song] }
71
+ let(:form) { AlbumForm.new(album) }
72
+
73
+ describe "#validate with invalid array property" do
74
+ it do
75
+ _(form.validate(
76
+ title: "Swimming Pool - EP",
77
+ band: {
78
+ name: "Marie Madeleine",
79
+ label: {name: "Ekler'o'shocK"}
80
+ },
81
+ artists: [42, "Good Charlotte", 43]
82
+ )).must_equal false
83
+ _(form.errors.messages).must_equal(artists: {0 => ["must be a string"], 2 => ["must be a string"]})
84
+ _(form.errors.size).must_equal(1)
85
+ end
86
+ end
87
+
88
+ describe "#errors without #validate" do
89
+ it do
90
+ _(form.errors.size).must_equal 0
91
+ end
92
+ end
93
+
94
+ describe "blank everywhere" do
95
+ before do
96
+ form.validate(
97
+ "hit" => {"title" => ""},
98
+ "title" => "",
99
+ "songs" => [{"title" => ""}, {"title" => ""}],
100
+ "producer" => {"name" => ""}
101
+ )
102
+ end
103
+
104
+ it do
105
+ _(form.errors.messages).must_equal(
106
+ title: ["must be filled"],
107
+ "hit.title": ["must be filled"],
108
+ "songs.title": ["must be filled"],
109
+ "band.label.name": ["must be filled"],
110
+ "producer.name": ["must be filled"]
111
+ )
112
+ end
113
+
114
+ # it do
115
+ # form.errors.must_equal({:title => ["must be filled"]})
116
+ # TODO: this should only contain local errors?
117
+ # end
118
+
119
+ # nested forms keep their own Errors:
120
+ it { _(form.producer.errors.messages).must_equal(name: ["must be filled"]) }
121
+ it { _(form.hit.errors.messages).must_equal(title: ["must be filled"]) }
122
+ it { _(form.songs[0].errors.messages).must_equal(title: ["must be filled"]) }
123
+
124
+ it do
125
+ _(form.errors.messages).must_equal(
126
+ title: ["must be filled"],
127
+ "hit.title": ["must be filled"],
128
+ "songs.title": ["must be filled"],
129
+ "band.label.name": ["must be filled"],
130
+ "producer.name": ["must be filled"]
131
+ )
132
+ _(form.errors.size).must_equal(5)
133
+ end
134
+ end
135
+
136
+ describe "#validate with main form invalid" do
137
+ it do
138
+ _(form.validate("title" => "", "band" => {"label" => {name: "Fat Wreck"}}, "producer" => nil)).must_equal false
139
+ _(form.errors.messages).must_equal(title: ["must be filled"], producer: ["must be a hash"])
140
+ _(form.errors.size).must_equal(2)
141
+ end
142
+ end
143
+
144
+ describe "#validate with middle nested form invalid" do
145
+ before { @result = form.validate("hit" => {"title" => ""}, "band" => {"label" => {name: "Fat Wreck"}}) }
146
+
147
+ it { _(@result).must_equal false }
148
+ it { _(form.errors.messages).must_equal("hit.title": ["must be filled"]) }
149
+ it { _(form.errors.size).must_equal(1) }
150
+ end
151
+
152
+ describe "#validate with collection form invalid" do
153
+ before { @result = form.validate("songs" => [{"title" => ""}], "band" => {"label" => {name: "Fat Wreck"}}) }
154
+
155
+ it { _(@result).must_equal false }
156
+ it { _(form.errors.messages).must_equal("songs.title": ["must be filled"]) }
157
+ it { _(form.errors.size).must_equal(1) }
158
+ end
159
+
160
+ describe "#validate with collection and 2-level-nested invalid" do
161
+ before { @result = form.validate("songs" => [{"title" => ""}], "band" => {"label" => {}}) }
162
+
163
+ it { _(@result).must_equal false }
164
+ it { _(form.errors.messages).must_equal("songs.title": ["must be filled"], "band.label.name": ["must be filled"]) }
165
+ it { _(form.errors.size).must_equal(2) }
166
+ end
167
+
168
+ describe "#validate with nested form using :base invalid" do
169
+ it do
170
+ result = form.validate("songs" => [{"title" => "Someday"}], "band" => {"name" => "Nickelback", "label" => {"name" => "Roadrunner Records"}})
171
+ _(result).must_equal false
172
+ _(form.errors.messages).must_equal("band.name": ["you're a bad person"])
173
+ _(form.errors.size).must_equal(1)
174
+ end
175
+ end
176
+
177
+ describe "#add" do
178
+ let(:album_title) { nil }
179
+ it do
180
+ result = form.validate("songs" => [{"title" => "Someday"}], "band" => {"name" => "Nickelback", "label" => {"name" => "Roadrunner Records"}})
181
+ _(result).must_equal false
182
+ _(form.errors.messages).must_equal(title: ["must be filled"], "band.name": ["you're a bad person"])
183
+ # add a new custom error
184
+ form.errors.add(:policy, "error_text")
185
+ _(form.errors.messages).must_equal(title: ["must be filled"], "band.name": ["you're a bad person"], policy: ["error_text"])
186
+ # does not duplicate errors
187
+ form.errors.add(:title, "must be filled")
188
+ _(form.errors.messages).must_equal(title: ["must be filled"], "band.name": ["you're a bad person"], policy: ["error_text"])
189
+ # merge existing errors
190
+ form.errors.add(:policy, "another error")
191
+ _(form.errors.messages).must_equal(title: ["must be filled"], "band.name": ["you're a bad person"], policy: ["error_text", "another error"])
192
+ end
193
+ end
194
+
195
+ describe "correct #validate" do
196
+ before do
197
+ @result = form.validate(
198
+ "hit" => {"title" => "Sacrifice"},
199
+ "title" => "Second Heat",
200
+ "songs" => [{"title" => "Heart Of A Lion"}],
201
+ "band" => {"label" => {name: "Fat Wreck"}}
202
+ )
203
+ end
204
+
205
+ it { _(@result).must_equal true }
206
+ it { _(form.hit.title).must_equal "Sacrifice" }
207
+ it { _(form.title).must_equal "Second Heat" }
208
+ it { _(form.songs.first.title).must_equal "Heart Of A Lion" }
209
+ it do
210
+ skip "WE DON'T NEED COUNT AND EMPTY? ON THE CORE ERRORS OBJECT"
211
+ _(form.errors.size).must_equal(0)
212
+ _(form.errors.empty?).must_equal(true)
213
+ end
214
+ end
215
+
216
+ describe "Errors#to_s" do
217
+ before { form.validate("songs" => [{"title" => ""}], "band" => {"label" => {}}) }
218
+
219
+ # to_s is aliased to messages
220
+ it {
221
+ skip "why do we need Errors#to_s ?"
222
+ _(form.errors.to_s).must_equal "{:\"songs.title\"=>[\"must be filled\"], :\"band.label.name\"=>[\"must be filled\"]}"
223
+ }
224
+ end
225
+ end