reform 2.2.4
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 +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
|