reform 2.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGES.md +415 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +339 -0
  8. data/Rakefile +15 -0
  9. data/TODO.md +45 -0
  10. data/gemfiles/Gemfile.disposable-0.3 +6 -0
  11. data/lib/reform.rb +8 -0
  12. data/lib/reform/contract.rb +77 -0
  13. data/lib/reform/contract/errors.rb +43 -0
  14. data/lib/reform/contract/validate.rb +33 -0
  15. data/lib/reform/form.rb +94 -0
  16. data/lib/reform/form/call.rb +23 -0
  17. data/lib/reform/form/coercion.rb +3 -0
  18. data/lib/reform/form/composition.rb +34 -0
  19. data/lib/reform/form/dry.rb +67 -0
  20. data/lib/reform/form/module.rb +27 -0
  21. data/lib/reform/form/mongoid.rb +37 -0
  22. data/lib/reform/form/orm.rb +26 -0
  23. data/lib/reform/form/populator.rb +123 -0
  24. data/lib/reform/form/prepopulate.rb +24 -0
  25. data/lib/reform/form/validate.rb +60 -0
  26. data/lib/reform/mongoid.rb +4 -0
  27. data/lib/reform/validation.rb +40 -0
  28. data/lib/reform/validation/groups.rb +73 -0
  29. data/lib/reform/version.rb +3 -0
  30. data/reform.gemspec +29 -0
  31. data/test/benchmarking.rb +26 -0
  32. data/test/call_test.rb +23 -0
  33. data/test/changed_test.rb +41 -0
  34. data/test/coercion_test.rb +66 -0
  35. data/test/composition_test.rb +149 -0
  36. data/test/contract_test.rb +77 -0
  37. data/test/default_test.rb +22 -0
  38. data/test/deprecation_test.rb +27 -0
  39. data/test/deserialize_test.rb +104 -0
  40. data/test/errors_test.rb +165 -0
  41. data/test/feature_test.rb +65 -0
  42. data/test/fixtures/dry_error_messages.yml +44 -0
  43. data/test/form_option_test.rb +24 -0
  44. data/test/form_test.rb +57 -0
  45. data/test/from_test.rb +75 -0
  46. data/test/inherit_test.rb +119 -0
  47. data/test/module_test.rb +142 -0
  48. data/test/parse_pipeline_test.rb +15 -0
  49. data/test/populate_test.rb +270 -0
  50. data/test/populator_skip_test.rb +28 -0
  51. data/test/prepopulator_test.rb +112 -0
  52. data/test/read_only_test.rb +3 -0
  53. data/test/readable_test.rb +30 -0
  54. data/test/readonly_test.rb +14 -0
  55. data/test/reform_test.rb +223 -0
  56. data/test/save_test.rb +89 -0
  57. data/test/setup_test.rb +48 -0
  58. data/test/skip_if_test.rb +74 -0
  59. data/test/skip_setter_and_getter_test.rb +54 -0
  60. data/test/test_helper.rb +49 -0
  61. data/test/validate_test.rb +420 -0
  62. data/test/validation/dry_test.rb +60 -0
  63. data/test/validation/dry_validation_test.rb +352 -0
  64. data/test/validation/errors.yml +4 -0
  65. data/test/virtual_test.rb +24 -0
  66. data/test/writeable_test.rb +29 -0
  67. 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