reform 2.3.0.rc1 → 2.5.0
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.
- checksums.yaml +5 -5
- data/.gitignore +5 -1
- data/.travis.yml +7 -11
- data/CHANGES.md +43 -3
- data/Gemfile +2 -5
- data/ISSUE_TEMPLATE.md +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +7 -9
- data/Rakefile +6 -10
- data/lib/reform/contract.rb +7 -7
- data/lib/reform/contract/custom_error.rb +41 -0
- data/lib/reform/contract/validate.rb +10 -6
- data/lib/reform/errors.rb +27 -15
- data/lib/reform/form.rb +22 -11
- data/lib/reform/form/call.rb +1 -1
- data/lib/reform/form/composition.rb +2 -2
- data/lib/reform/form/dry.rb +22 -60
- data/lib/reform/form/dry/input_hash.rb +37 -0
- data/lib/reform/form/populator.rb +9 -11
- data/lib/reform/form/prepopulate.rb +3 -2
- data/lib/reform/form/validate.rb +19 -12
- data/lib/reform/result.rb +36 -9
- data/lib/reform/validation.rb +10 -8
- data/lib/reform/validation/groups.rb +2 -4
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +9 -9
- data/test/benchmarking.rb +10 -11
- data/test/call_test.rb +8 -8
- data/test/changed_test.rb +13 -13
- data/test/coercion_test.rb +56 -24
- data/test/composition_test.rb +49 -51
- data/test/contract/custom_error_test.rb +55 -0
- data/test/contract_test.rb +18 -18
- data/test/default_test.rb +3 -3
- data/test/deserialize_test.rb +14 -17
- data/test/docs/validation_test.rb +134 -0
- data/test/errors_test.rb +131 -86
- data/test/feature_test.rb +9 -11
- data/test/fixtures/dry_error_messages.yml +65 -52
- data/test/form_option_test.rb +3 -3
- data/test/form_test.rb +6 -6
- data/test/from_test.rb +17 -21
- data/test/inherit_test.rb +28 -35
- data/test/module_test.rb +23 -28
- data/test/parse_option_test.rb +12 -12
- data/test/parse_pipeline_test.rb +3 -3
- data/test/populate_test.rb +146 -93
- data/test/populator_skip_test.rb +3 -4
- data/test/prepopulator_test.rb +20 -21
- data/test/read_only_test.rb +12 -1
- data/test/readable_test.rb +7 -7
- data/test/reform_test.rb +38 -42
- data/test/save_test.rb +16 -19
- data/test/setup_test.rb +15 -15
- data/test/skip_if_test.rb +30 -19
- data/test/skip_setter_and_getter_test.rb +8 -9
- data/test/test_helper.rb +12 -5
- data/test/validate_test.rb +160 -140
- data/test/validation/dry_validation_test.rb +407 -236
- data/test/validation/result_test.rb +29 -31
- data/test/validation_library_provided_test.rb +3 -3
- data/test/virtual_test.rb +46 -6
- data/test/writeable_test.rb +13 -13
- metadata +32 -29
- data/test/readonly_test.rb +0 -14
data/lib/reform/version.rb
CHANGED
data/reform.gemspec
CHANGED
@@ -1,30 +1,30 @@
|
|
1
|
-
lib = File.expand_path(
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
require
|
3
|
+
require "reform/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = "reform"
|
7
7
|
spec.version = Reform::VERSION
|
8
8
|
spec.authors = ["Nick Sutterer", "Fran Worley"]
|
9
9
|
spec.email = ["apotonick@gmail.com", "frances@safetytoolbox.co.uk"]
|
10
|
-
spec.description =
|
11
|
-
spec.summary =
|
10
|
+
spec.description = "Form object decoupled from models."
|
11
|
+
spec.summary = "Form object decoupled from models with validation, population and presentation."
|
12
12
|
spec.homepage = "https://github.com/trailblazer/reform"
|
13
13
|
spec.license = "MIT"
|
14
14
|
|
15
15
|
spec.files = `git ls-files`.split($/)
|
16
|
-
spec.executables = spec.files.grep(%r
|
16
|
+
spec.executables = spec.files.grep(%r(^bin/)) { |f| File.basename(f) }
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
20
|
spec.add_dependency "disposable", ">= 0.4.2", "< 0.5.0"
|
21
|
-
spec.add_dependency "uber", "< 0.2.0"
|
22
21
|
spec.add_dependency "representable", ">= 2.4.0", "< 3.1.0"
|
22
|
+
spec.add_dependency "uber", "< 0.2.0"
|
23
23
|
|
24
24
|
spec.add_development_dependency "bundler"
|
25
|
-
spec.add_development_dependency "rake"
|
26
25
|
spec.add_development_dependency "minitest"
|
27
|
-
spec.add_development_dependency "
|
26
|
+
spec.add_development_dependency "minitest-line"
|
27
|
+
spec.add_development_dependency "pry-byebug"
|
28
28
|
spec.add_development_dependency "multi_json"
|
29
|
-
spec.add_development_dependency "
|
29
|
+
spec.add_development_dependency "rake"
|
30
30
|
end
|
data/test/benchmarking.rb
CHANGED
@@ -1,29 +1,29 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "reform"
|
2
|
+
require "benchmark/ips"
|
3
3
|
require "reform/form/dry"
|
4
4
|
|
5
5
|
class BandForm < Reform::Form
|
6
6
|
feature Reform::Form::Dry
|
7
|
-
property :name#, validates: {presence: true}
|
7
|
+
property :name #, validates: {presence: true}
|
8
8
|
collection :songs do
|
9
|
-
property :title#, validates: {presence: true}
|
9
|
+
property :title #, validates: {presence: true}
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
13
|
class OptimizedBandForm < Reform::Form
|
14
14
|
feature Reform::Form::Dry
|
15
|
-
property :name#, validates: {presence: true}
|
15
|
+
property :name #, validates: {presence: true}
|
16
16
|
collection :songs do
|
17
|
-
property :title#, validates: {presence: true}
|
18
|
-
|
17
|
+
property :title #, validates: {presence: true}
|
18
|
+
|
19
19
|
def deserializer(*args)
|
20
|
-
# DISCUSS: should we simply delegate to class and sort out memoizing there?
|
20
|
+
# DISCUSS: should we simply delegate to class and sort out memoizing there?
|
21
21
|
self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
|
22
|
-
|
22
|
+
end
|
23
23
|
end
|
24
24
|
|
25
25
|
def deserializer(*args)
|
26
|
-
# DISCUSS: should we simply delegate to class and sort out memoizing there?
|
26
|
+
# DISCUSS: should we simply delegate to class and sort out memoizing there?
|
27
27
|
self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
|
28
28
|
end
|
29
29
|
end
|
@@ -43,7 +43,6 @@ end
|
|
43
43
|
|
44
44
|
exit
|
45
45
|
|
46
|
-
|
47
46
|
songs = 50.times.collect { OpenStruct.new(title: "Be Stag") }
|
48
47
|
band = OpenStruct.new(name: "Teenage Bottlerock", songs: songs)
|
49
48
|
|
data/test/call_test.rb
CHANGED
@@ -7,17 +7,17 @@ class CallTest < Minitest::Spec
|
|
7
7
|
property :title
|
8
8
|
|
9
9
|
validation do
|
10
|
-
|
10
|
+
params { required(:title).filled }
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
let
|
14
|
+
let(:form) { SongForm.new(Song.new) }
|
15
15
|
|
16
|
-
it { form.(title: "True North").success
|
17
|
-
it { form.(title: "True North").failure
|
18
|
-
it { form.(title: "").success
|
19
|
-
it { form.(title: "").failure
|
16
|
+
it { assert form.(title: "True North").success? }
|
17
|
+
it { refute form.(title: "True North").failure? }
|
18
|
+
it { refute form.(title: "").success? }
|
19
|
+
it { assert form.(title: "").failure? }
|
20
20
|
|
21
|
-
it { form.(title: "True North").errors.messages
|
22
|
-
it { form.(title: "").errors.messages
|
21
|
+
it { assert_equal form.(title: "True North").errors.messages, {} }
|
22
|
+
it { assert_equal form.(title: "").errors.messages, title: ["must be filled"] }
|
23
23
|
end
|
data/test/changed_test.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
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)
|
@@ -18,24 +18,24 @@ class ChangedTest < MiniTest::Spec
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
let
|
22
|
-
let
|
23
|
-
let
|
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
|
25
|
+
let(:form) { AlbumForm.new(album) }
|
26
26
|
|
27
27
|
# nothing changed after setup.
|
28
28
|
it do
|
29
|
-
form.changed?(:name)
|
30
|
-
form.songs[0].changed?(:title)
|
31
|
-
form.songs[0].composer.changed?(:name)
|
29
|
+
refute form.changed?(:name)
|
30
|
+
refute form.songs[0].changed?(:title)
|
31
|
+
refute form.songs[0].composer.changed?(:name)
|
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)
|
38
|
-
form.songs[0].changed?(:title)
|
39
|
-
form.songs[0].composer.changed?(:name)
|
37
|
+
assert form.changed?(:name)
|
38
|
+
refute form.songs[0].changed?(:title)
|
39
|
+
assert form.songs[0].composer.changed?(:name)
|
40
40
|
end
|
41
|
-
end
|
41
|
+
end
|
data/test/coercion_test.rb
CHANGED
@@ -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
12
|
class Form < TestForm
|
12
13
|
feature Coercion
|
14
|
+
include Disposable::Twin::Property::Hash
|
13
15
|
|
14
|
-
property :released_at, type: Types::
|
16
|
+
property :released_at, type: Types::Params::DateTime
|
15
17
|
|
16
18
|
property :hit do
|
17
|
-
property :length, type: Types::
|
18
|
-
property :good, type: Types::
|
19
|
+
property :length, type: Types::Params::Integer
|
20
|
+
property :good, type: Types::Params::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: Types::Params::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
|
40
|
+
let(:album) do
|
33
41
|
OpenStruct.new(
|
34
|
-
:
|
35
|
-
:
|
36
|
-
:
|
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
|
42
|
-
it { subject.hit.length
|
43
|
-
it { subject.band.label.value
|
44
|
-
|
50
|
+
it { assert_equal subject.released_at, "31/03/1981" } # NO coercion in setup.
|
51
|
+
it { assert_equal subject.hit.length, "312" }
|
52
|
+
it { assert_equal subject.band.label.value, "9999.99" }
|
45
53
|
|
46
|
-
let
|
54
|
+
let(:params) do
|
47
55
|
{
|
48
|
-
:
|
49
|
-
:
|
50
|
-
|
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
|
60
|
-
it { subject.hit.length
|
61
|
-
it {
|
62
|
-
it { subject.band.label.value
|
78
|
+
it { assert_equal subject.released_at, DateTime.parse("30/03/1981") }
|
79
|
+
it { assert_equal subject.hit.length, 312 }
|
80
|
+
it { assert_equal subject.hit.good, false }
|
81
|
+
it { assert_equal subject.band.label.value, "9999.999999.99" } # coercion happened once.
|
82
|
+
it { assert_equal subject.metadata.publication_settings.featured, false }
|
63
83
|
end
|
64
84
|
|
65
|
-
#
|
85
|
+
# sync
|
86
|
+
describe "#sync" do
|
87
|
+
before do
|
88
|
+
assert subject.validate(params)
|
89
|
+
subject.sync
|
90
|
+
end
|
91
|
+
|
92
|
+
it { assert_equal album.released_at, DateTime.parse("30/03/1981") }
|
93
|
+
it { assert_equal album.hit.length, 312 }
|
94
|
+
it { assert_equal album.hit.good, false }
|
95
|
+
it { assert_nil album.metadata[:publication_settings] }
|
96
|
+
it { assert_equal album.metadata["publication_settings"]["featured"], false }
|
97
|
+
end
|
66
98
|
end
|
data/test/composition_test.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "test_helper"
|
2
2
|
|
3
3
|
class FormCompositionInheritanceTest < MiniTest::Spec
|
4
4
|
module SizePrice
|
@@ -26,16 +26,15 @@ class FormCompositionInheritanceTest < MiniTest::Spec
|
|
26
26
|
property :size, inherit: true, on: :measurement
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
let
|
31
|
-
let
|
32
|
-
let (:form) { OutfitForm.new(tshirt: tshirt, measurement: measurement) }
|
29
|
+
let(:measurement) { Measurement.new(:l) }
|
30
|
+
let(:tshirt) { Tshirt.new(2, :m) }
|
31
|
+
let(:form) { OutfitForm.new(tshirt: tshirt, measurement: measurement) }
|
33
32
|
|
34
33
|
Tshirt = Struct.new(:price, :size)
|
35
34
|
Measurement = Struct.new(:size)
|
36
35
|
|
37
|
-
it { form.price
|
38
|
-
it { form.price(for_size: :s)
|
36
|
+
it { assert_equal form.price, 6 }
|
37
|
+
it { assert_equal form.price(for_size: :s), 2 }
|
39
38
|
end
|
40
39
|
|
41
40
|
class FormCompositionTest < MiniTest::Spec
|
@@ -46,49 +45,50 @@ class FormCompositionTest < MiniTest::Spec
|
|
46
45
|
class RequestForm < TestForm
|
47
46
|
include Composition
|
48
47
|
|
49
|
-
property :name, :
|
50
|
-
property :requester_id, :
|
51
|
-
properties :title, :id, :
|
48
|
+
property :name, on: :requester
|
49
|
+
property :requester_id, on: :requester, from: :id
|
50
|
+
properties :title, :id, on: :song
|
52
51
|
# property :channel # FIXME: what about the "main model"?
|
53
|
-
property :channel, :
|
54
|
-
property :requester, :
|
55
|
-
property :captcha, :
|
52
|
+
property :channel, virtual: true, on: :song
|
53
|
+
property :requester, on: :requester
|
54
|
+
property :captcha, on: :song, virtual: true
|
56
55
|
|
57
56
|
validation do
|
58
|
-
|
59
|
-
|
57
|
+
params do
|
58
|
+
required(:name).filled
|
59
|
+
required(:title).filled
|
60
|
+
end
|
60
61
|
end
|
61
62
|
|
62
|
-
property :band,
|
63
|
+
property :band, on: :song do
|
63
64
|
property :title
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
67
|
-
let
|
68
|
-
let
|
69
|
-
let
|
70
|
-
let
|
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") }
|
71
72
|
|
72
73
|
# delegation form -> composition works
|
73
|
-
it { form.id
|
74
|
-
it { form.title
|
75
|
-
it { form.name
|
76
|
-
it { form.requester_id
|
74
|
+
it { assert_equal form.id, 1 }
|
75
|
+
it { assert_equal form.title, "Rio" }
|
76
|
+
it { assert_equal form.name, "Duran Duran" }
|
77
|
+
it { assert_equal form.requester_id, 2 }
|
77
78
|
it { assert_nil form.channel }
|
78
|
-
it { form.requester
|
79
|
+
it { assert_equal form.requester, "MCP" } # same name as composed model.
|
79
80
|
it { assert_nil form.captcha }
|
80
81
|
|
81
82
|
# #model just returns <Composition>.
|
82
|
-
it { form.mapper.
|
83
|
+
it { assert form.mapper.is_a? Disposable::Composition }
|
83
84
|
|
84
85
|
# #model[] -> composed models
|
85
|
-
it { form.model[:requester]
|
86
|
-
it { form.model[:song]
|
87
|
-
|
86
|
+
it { assert_equal form.model[:requester], requester }
|
87
|
+
it { assert_equal form.model[:song], song }
|
88
88
|
|
89
89
|
it "creates Composition for you" do
|
90
|
-
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb")
|
91
|
-
form.validate("title" => "", "name" => "Frenzal Rhomb")
|
90
|
+
assert_equal form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb"), true
|
91
|
+
assert_equal form.validate("title" => "", "name" => "Frenzal Rhomb"), false
|
92
92
|
end
|
93
93
|
|
94
94
|
describe "#save" do
|
@@ -101,7 +101,7 @@ class FormCompositionTest < MiniTest::Spec
|
|
101
101
|
hash[:title] = form.title
|
102
102
|
end
|
103
103
|
|
104
|
-
hash
|
104
|
+
assert_equal hash, name: "Duran Duran", title: "Rio"
|
105
105
|
end
|
106
106
|
|
107
107
|
it "provides nested symbolized hash as second block argument" do
|
@@ -113,11 +113,10 @@ class FormCompositionTest < MiniTest::Spec
|
|
113
113
|
hash = map
|
114
114
|
end
|
115
115
|
|
116
|
-
hash
|
117
|
-
:
|
118
|
-
:
|
119
|
-
|
120
|
-
)
|
116
|
+
assert_equal hash, {
|
117
|
+
song: {"title" => "Greyhound", "id" => 1, "channel" => "JJJ", "captcha" => "wonderful", "band" => {"title" => "Duran^2"}},
|
118
|
+
requester: {"name" => "Frenzal Rhomb", "id" => 2, "requester" => "MCP"}
|
119
|
+
}
|
121
120
|
end
|
122
121
|
|
123
122
|
it "xxx pushes data to models and calls #save when no block passed" do
|
@@ -126,16 +125,16 @@ class FormCompositionTest < MiniTest::Spec
|
|
126
125
|
band.extend(Saveable)
|
127
126
|
|
128
127
|
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
|
129
|
-
form.captcha
|
128
|
+
assert_equal form.captcha, "1337" # TODO: move to separate test.
|
130
129
|
|
131
130
|
form.save
|
132
131
|
|
133
|
-
requester.name
|
134
|
-
requester.saved
|
135
|
-
song.title
|
136
|
-
song.saved
|
137
|
-
song.band.title
|
138
|
-
song.band.saved
|
132
|
+
assert_equal requester.name, "Frenzal Rhomb"
|
133
|
+
assert_equal requester.saved?, true
|
134
|
+
assert_equal song.title, "Greyhound"
|
135
|
+
assert_equal song.saved?, true
|
136
|
+
assert_equal song.band.title, "Duran^2"
|
137
|
+
assert_equal song.band.saved?, true
|
139
138
|
end
|
140
139
|
|
141
140
|
it "returns true when models all save successfully" do
|
@@ -143,7 +142,7 @@ class FormCompositionTest < MiniTest::Spec
|
|
143
142
|
requester.extend(Saveable)
|
144
143
|
band.extend(Saveable)
|
145
144
|
|
146
|
-
form.save
|
145
|
+
assert_equal form.save, true
|
147
146
|
end
|
148
147
|
|
149
148
|
it "returns false when one or more models don't save successfully" do
|
@@ -157,17 +156,16 @@ class FormCompositionTest < MiniTest::Spec
|
|
157
156
|
requester.extend(Saveable)
|
158
157
|
band.extend(Saveable)
|
159
158
|
|
160
|
-
form.save
|
159
|
+
assert_equal form.save, false
|
161
160
|
end
|
162
161
|
end
|
163
162
|
end
|
164
163
|
|
165
|
-
|
166
164
|
class FormCompositionCollectionTest < MiniTest::Spec
|
167
165
|
Book = Struct.new(:id, :name)
|
168
166
|
Library = Struct.new(:id) do
|
169
167
|
def books
|
170
|
-
[Book.new(1,"My book")]
|
168
|
+
[Book.new(1, "My book")]
|
171
169
|
end
|
172
170
|
end
|
173
171
|
|
@@ -180,8 +178,8 @@ class FormCompositionCollectionTest < MiniTest::Spec
|
|
180
178
|
end
|
181
179
|
end
|
182
180
|
|
183
|
-
let
|
184
|
-
let
|
181
|
+
let(:form) { LibraryForm.new(library: library) }
|
182
|
+
let(:library) { Library.new(2) }
|
185
183
|
|
186
|
-
it { form.save
|
184
|
+
it { form.save { |hash| assert_equal hash, { library: { "books" => [{ "id" => 1, "name" => "My book" }] } } } }
|
187
185
|
end
|