reform 2.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +11 -0
- data/CHANGES.md +415 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +339 -0
- data/Rakefile +15 -0
- data/TODO.md +45 -0
- data/gemfiles/Gemfile.disposable-0.3 +6 -0
- data/lib/reform.rb +8 -0
- data/lib/reform/contract.rb +77 -0
- data/lib/reform/contract/errors.rb +43 -0
- data/lib/reform/contract/validate.rb +33 -0
- data/lib/reform/form.rb +94 -0
- data/lib/reform/form/call.rb +23 -0
- data/lib/reform/form/coercion.rb +3 -0
- data/lib/reform/form/composition.rb +34 -0
- data/lib/reform/form/dry.rb +67 -0
- data/lib/reform/form/module.rb +27 -0
- data/lib/reform/form/mongoid.rb +37 -0
- data/lib/reform/form/orm.rb +26 -0
- data/lib/reform/form/populator.rb +123 -0
- data/lib/reform/form/prepopulate.rb +24 -0
- data/lib/reform/form/validate.rb +60 -0
- data/lib/reform/mongoid.rb +4 -0
- data/lib/reform/validation.rb +40 -0
- data/lib/reform/validation/groups.rb +73 -0
- data/lib/reform/version.rb +3 -0
- data/reform.gemspec +29 -0
- data/test/benchmarking.rb +26 -0
- data/test/call_test.rb +23 -0
- data/test/changed_test.rb +41 -0
- data/test/coercion_test.rb +66 -0
- data/test/composition_test.rb +149 -0
- data/test/contract_test.rb +77 -0
- data/test/default_test.rb +22 -0
- data/test/deprecation_test.rb +27 -0
- data/test/deserialize_test.rb +104 -0
- data/test/errors_test.rb +165 -0
- data/test/feature_test.rb +65 -0
- data/test/fixtures/dry_error_messages.yml +44 -0
- data/test/form_option_test.rb +24 -0
- data/test/form_test.rb +57 -0
- data/test/from_test.rb +75 -0
- data/test/inherit_test.rb +119 -0
- data/test/module_test.rb +142 -0
- data/test/parse_pipeline_test.rb +15 -0
- data/test/populate_test.rb +270 -0
- data/test/populator_skip_test.rb +28 -0
- data/test/prepopulator_test.rb +112 -0
- data/test/read_only_test.rb +3 -0
- data/test/readable_test.rb +30 -0
- data/test/readonly_test.rb +14 -0
- data/test/reform_test.rb +223 -0
- data/test/save_test.rb +89 -0
- data/test/setup_test.rb +48 -0
- data/test/skip_if_test.rb +74 -0
- data/test/skip_setter_and_getter_test.rb +54 -0
- data/test/test_helper.rb +49 -0
- data/test/validate_test.rb +420 -0
- data/test/validation/dry_test.rb +60 -0
- data/test/validation/dry_validation_test.rb +352 -0
- data/test/validation/errors.yml +4 -0
- data/test/virtual_test.rb +24 -0
- data/test/writeable_test.rb +29 -0
- metadata +265 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "reform/form/coercion"
|
3
|
+
|
4
|
+
class CoercionTest < BaseTest
|
5
|
+
class Irreversible
|
6
|
+
def self.call(value)
|
7
|
+
value*2
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Form < Reform::Form
|
12
|
+
feature Coercion
|
13
|
+
|
14
|
+
property :released_at, type: Types::Form::DateTime
|
15
|
+
|
16
|
+
property :hit do
|
17
|
+
property :length, type: Types::Form::Int
|
18
|
+
property :good, type: Types::Form::Bool
|
19
|
+
end
|
20
|
+
|
21
|
+
property :band do
|
22
|
+
property :label do
|
23
|
+
property :value, type: Irreversible
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
subject do
|
29
|
+
Form.new(album)
|
30
|
+
end
|
31
|
+
|
32
|
+
let (:album) {
|
33
|
+
OpenStruct.new(
|
34
|
+
:released_at => "31/03/1981",
|
35
|
+
:hit => OpenStruct.new(:length => "312"),
|
36
|
+
:band => Band.new(OpenStruct.new(:value => "9999.99"))
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
# 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
|
+
|
45
|
+
|
46
|
+
let (:params) {
|
47
|
+
{
|
48
|
+
:released_at => "30/03/1981",
|
49
|
+
:hit => {:length => "312"},
|
50
|
+
:band => {:label => {:value => "9999.99"}}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
# validate
|
56
|
+
describe "#validate" do
|
57
|
+
before { subject.validate(params) }
|
58
|
+
|
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.
|
63
|
+
end
|
64
|
+
|
65
|
+
# save
|
66
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FormCompositionTest < MiniTest::Spec
|
4
|
+
Song = Struct.new(:id, :title, :band)
|
5
|
+
Requester = Struct.new(:id, :name, :requester)
|
6
|
+
Band = Struct.new(:title)
|
7
|
+
|
8
|
+
class RequestForm < Reform::Form
|
9
|
+
include Composition
|
10
|
+
|
11
|
+
property :name, :on => :requester
|
12
|
+
property :requester_id, :on => :requester, :from => :id
|
13
|
+
properties :title, :id, :on => :song
|
14
|
+
# property :channel # FIXME: what about the "main model"?
|
15
|
+
property :channel, :virtual => true, :on => :song
|
16
|
+
property :requester, :on => :requester
|
17
|
+
property :captcha, :on => :song, :virtual => true
|
18
|
+
|
19
|
+
validation do
|
20
|
+
key(:name).required
|
21
|
+
key(:name).required
|
22
|
+
key(:title).required
|
23
|
+
end
|
24
|
+
|
25
|
+
property :band, :on => :song do
|
26
|
+
property :title
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
let (:form) { RequestForm.new(:song => song, :requester => requester) }
|
31
|
+
let (:song) { Song.new(1, "Rio", band) }
|
32
|
+
let (:requester) { Requester.new(2, "Duran Duran", "MCP") }
|
33
|
+
let (:band) { Band.new("Duran^2") }
|
34
|
+
|
35
|
+
# delegation form -> composition works
|
36
|
+
it { form.id.must_equal 1 }
|
37
|
+
it { form.title.must_equal "Rio" }
|
38
|
+
it { form.name.must_equal "Duran Duran" }
|
39
|
+
it { form.requester_id.must_equal 2 }
|
40
|
+
it { form.channel.must_equal nil }
|
41
|
+
it { form.requester.must_equal "MCP" } # same name as composed model.
|
42
|
+
it { form.captcha.must_equal nil }
|
43
|
+
|
44
|
+
# #model just returns <Composition>.
|
45
|
+
it { form.mapper.must_be_kind_of Disposable::Composition }
|
46
|
+
|
47
|
+
# #model[] -> composed models
|
48
|
+
it { form.model[:requester].must_equal requester }
|
49
|
+
it { form.model[:song].must_equal song }
|
50
|
+
|
51
|
+
|
52
|
+
it "creates Composition for you" do
|
53
|
+
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal false
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#save" do
|
57
|
+
# #save with {}
|
58
|
+
it do
|
59
|
+
hash = {}
|
60
|
+
|
61
|
+
form.save do |map|
|
62
|
+
hash[:name] = form.name
|
63
|
+
hash[:title] = form.title
|
64
|
+
end
|
65
|
+
|
66
|
+
hash.must_equal({:name=>"Duran Duran", :title=>"Rio"})
|
67
|
+
end
|
68
|
+
|
69
|
+
it "provides nested symbolized hash as second block argument" do
|
70
|
+
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "channel" => "JJJ", "captcha" => "wonderful")
|
71
|
+
|
72
|
+
hash = nil
|
73
|
+
|
74
|
+
form.save do |map|
|
75
|
+
hash = map
|
76
|
+
end
|
77
|
+
|
78
|
+
hash.must_equal({
|
79
|
+
:song=>{"title"=>"Greyhound", "id"=>1, "channel" => "JJJ", "captcha"=>"wonderful", "band"=>{"title"=>"Duran^2"}},
|
80
|
+
:requester=>{"name"=>"Frenzal Rhomb", "id"=>2, "requester" => "MCP"}
|
81
|
+
}
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "xxx pushes data to models and calls #save when no block passed" do
|
86
|
+
song.extend(Saveable)
|
87
|
+
requester.extend(Saveable)
|
88
|
+
band.extend(Saveable)
|
89
|
+
|
90
|
+
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
|
91
|
+
form.captcha.must_equal "1337" # TODO: move to separate test.
|
92
|
+
|
93
|
+
form.save
|
94
|
+
|
95
|
+
requester.name.must_equal "Frenzal Rhomb"
|
96
|
+
requester.saved?.must_equal true
|
97
|
+
song.title.must_equal "Greyhound"
|
98
|
+
song.saved?.must_equal true
|
99
|
+
song.band.title.must_equal "Duran^2"
|
100
|
+
song.band.saved?.must_equal true
|
101
|
+
end
|
102
|
+
|
103
|
+
it "returns true when models all save successfully" do
|
104
|
+
song.extend(Saveable)
|
105
|
+
requester.extend(Saveable)
|
106
|
+
band.extend(Saveable)
|
107
|
+
|
108
|
+
form.save.must_equal true
|
109
|
+
end
|
110
|
+
|
111
|
+
it "returns false when one or more models don't save successfully" do
|
112
|
+
module Unsaveable
|
113
|
+
def save
|
114
|
+
false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
song.extend(Unsaveable)
|
119
|
+
requester.extend(Saveable)
|
120
|
+
band.extend(Saveable)
|
121
|
+
|
122
|
+
form.save.must_equal false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
class FormCompositionCollectionTest < MiniTest::Spec
|
129
|
+
Book = Struct.new(:id, :name)
|
130
|
+
Library = Struct.new(:id) do
|
131
|
+
def books
|
132
|
+
[Book.new(1,"My book")]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class LibraryForm < Reform::Form
|
137
|
+
include Reform::Form::Composition
|
138
|
+
|
139
|
+
collection :books, on: :library do
|
140
|
+
property :id
|
141
|
+
property :name
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
let (:form) { LibraryForm.new(library: library) }
|
146
|
+
let (:library) { Library.new(2) }
|
147
|
+
|
148
|
+
it { form.save do |hash| hash.must_equal({:library=>{"books"=>[{"id"=>1, "name"=>"My book"}]}}) end }
|
149
|
+
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 < Reform::Form
|
9
|
+
property :name
|
10
|
+
end
|
11
|
+
|
12
|
+
class AlbumForm < Reform::Contract
|
13
|
+
property :name
|
14
|
+
|
15
|
+
properties :duration
|
16
|
+
properties :year, :style, readable: false
|
17
|
+
|
18
|
+
validation do
|
19
|
+
key(:name).required
|
20
|
+
end
|
21
|
+
|
22
|
+
collection :songs do
|
23
|
+
property :title
|
24
|
+
validation do
|
25
|
+
key(:title).required
|
26
|
+
end
|
27
|
+
|
28
|
+
property :composer do
|
29
|
+
property :name
|
30
|
+
validation do
|
31
|
+
key(:name).required
|
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 [: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,22 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class DefaultTest < 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 < Reform::Form
|
9
|
+
property :name, default: "Wrong"
|
10
|
+
|
11
|
+
collection :songs do
|
12
|
+
property :title, default: "It's Catching Up"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it do
|
17
|
+
form = AlbumForm.new(Album.new(nil, [Song.new]))
|
18
|
+
|
19
|
+
form.name.must_equal "Wrong"
|
20
|
+
form.songs[0].title.must_equal "It's Catching Up"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
|
4
|
+
class DeprecationRemoveMePopulatorTest < MiniTest::Spec
|
5
|
+
Album = Struct.new(:songs)
|
6
|
+
Song = Struct.new(:title)
|
7
|
+
|
8
|
+
|
9
|
+
class AlbumForm < Reform::Form
|
10
|
+
collection :songs, populator: ->(fragment, collection, index, *) { return Representable::Pipeline::Stop if fragment[:title]=="Good"
|
11
|
+
songs[index]
|
12
|
+
} do
|
13
|
+
property :title
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it do
|
18
|
+
form = AlbumForm.new(Album.new([Song.new, Song.new]))
|
19
|
+
hash = {songs: [{title: "Good"}, {title: "Bad"}]}
|
20
|
+
|
21
|
+
form.validate(hash)
|
22
|
+
|
23
|
+
form.songs.size.must_equal 2
|
24
|
+
form.songs[0].title.must_equal nil
|
25
|
+
form.songs[1].title.must_equal "Bad"
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require "representable/json"
|
3
|
+
|
4
|
+
class DeserializeTest < MiniTest::Spec
|
5
|
+
Song = Struct.new(:title, :album, :composer)
|
6
|
+
Album = Struct.new(:title, :artist)
|
7
|
+
Artist = Struct.new(:name, :callname)
|
8
|
+
|
9
|
+
class JsonAlbumForm < Reform::Form
|
10
|
+
module Json
|
11
|
+
def deserialize(params)
|
12
|
+
deserializer.new(self).
|
13
|
+
# extend(Representable::Debug).
|
14
|
+
from_json(params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def deserializer
|
18
|
+
Disposable::Rescheme.from(self.class,
|
19
|
+
include: [Representable::JSON],
|
20
|
+
superclass: Representable::Decorator,
|
21
|
+
definitions_from: lambda { |inline| inline.definitions },
|
22
|
+
options_from: :deserializer,
|
23
|
+
exclude_options: [:populator]
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
include Json
|
28
|
+
|
29
|
+
|
30
|
+
property :title
|
31
|
+
property :artist, populate_if_empty: Artist do
|
32
|
+
property :name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
let (:artist) { Artist.new("A-ha") }
|
38
|
+
it do
|
39
|
+
artist_id = artist.object_id
|
40
|
+
|
41
|
+
form = JsonAlbumForm.new(Album.new("Best Of", artist))
|
42
|
+
json = MultiJson.dump({title: "Apocalypse Soon", artist: {name: "Mute"}})
|
43
|
+
|
44
|
+
form.validate(json)
|
45
|
+
|
46
|
+
form.title.must_equal "Apocalypse Soon"
|
47
|
+
form.artist.name.must_equal "Mute"
|
48
|
+
form.artist.model.object_id.must_equal artist_id
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "infering the deserializer from another form should NOT copy its populators" do
|
52
|
+
class CompilationForm < Reform::Form
|
53
|
+
property :artist, populator: ->(options) { self.artist = Artist.new(nil, options[:fragment].to_s) } do
|
54
|
+
property :name
|
55
|
+
end
|
56
|
+
|
57
|
+
def deserializer
|
58
|
+
super(JsonAlbumForm, include: [Representable::Hash])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# also tests the Form#deserializer API. # FIXME.
|
63
|
+
it "uses deserializer inferred from JsonAlbumForm but deserializes/populates to CompilationForm" do
|
64
|
+
form = CompilationForm.new(Album.new)
|
65
|
+
form.validate("artist"=> {"name" => "Horowitz"}) # the deserializer doesn't know symbols.
|
66
|
+
form.sync
|
67
|
+
form.artist.model.must_equal Artist.new("Horowitz", %{{"name"=>"Horowitz"}})
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
class ValidateWithBlockTest < MiniTest::Spec
|
74
|
+
Song = Struct.new(:title, :album, :composer)
|
75
|
+
Album = Struct.new(:title, :artist)
|
76
|
+
Artist = Struct.new(:name)
|
77
|
+
|
78
|
+
class AlbumForm < Reform::Form
|
79
|
+
property :title
|
80
|
+
property :artist, populate_if_empty: Artist do
|
81
|
+
property :name
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it do
|
86
|
+
album = Album.new
|
87
|
+
form = AlbumForm.new(album)
|
88
|
+
json = MultiJson.dump({title: "Apocalypse Soon", artist: {name: "Mute"}})
|
89
|
+
|
90
|
+
deserializer = Disposable::Rescheme.from(AlbumForm,
|
91
|
+
include: [Representable::JSON],
|
92
|
+
superclass: Representable::Decorator,
|
93
|
+
definitions_from: lambda { |inline| inline.definitions },
|
94
|
+
options_from: :deserializer
|
95
|
+
)
|
96
|
+
|
97
|
+
form.validate(json) do |params|
|
98
|
+
deserializer.new(form).from_json(params)
|
99
|
+
end.must_equal true # with block must return result, too.
|
100
|
+
|
101
|
+
form.title.must_equal "Apocalypse Soon"
|
102
|
+
form.artist.name.must_equal "Mute"
|
103
|
+
end
|
104
|
+
end
|