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,165 @@
1
+ require "test_helper"
2
+
3
+ class ErrorsTest < MiniTest::Spec
4
+ class AlbumForm < Reform::Form
5
+ property :title
6
+
7
+ property :hit do
8
+ property :title
9
+ validation do
10
+ required(:title).filled
11
+ end
12
+ end
13
+
14
+ collection :songs do
15
+ property :title
16
+ validation do
17
+ required(:title).filled
18
+ end
19
+ end
20
+
21
+ property :band do # yepp, people do crazy stuff like that.
22
+ property :name
23
+ property :label do
24
+ property :name
25
+ validation do
26
+ required(:name).filled
27
+ end
28
+ end
29
+ # TODO: make band a required object.
30
+
31
+ validation do
32
+ # required(:name).filled(:music_taste_ok?)
33
+
34
+ configure do
35
+ config.messages_file = "test/validation/errors.yml"
36
+
37
+ def music_taste_ok?(value)
38
+ value != "Nickelback"
39
+ # errors.add(:base, "You are a bad person") if name == "Nickelback"
40
+ end
41
+ end
42
+ end
43
+ # validate :music_taste_ok?
44
+
45
+ # private
46
+ # def music_taste_ok?
47
+ # errors.add(:base, "You are a bad person") if name == "Nickelback"
48
+ # end
49
+ end
50
+
51
+ validation do
52
+ required(:title).filled
53
+ end
54
+ end
55
+
56
+ let (:album) do
57
+ OpenStruct.new(
58
+ :title => "Blackhawks Over Los Angeles",
59
+ :hit => song,
60
+ :songs => songs, # TODO: document this requirement,
61
+
62
+ :band => Struct.new(:name, :label).new("Epitaph", OpenStruct.new),
63
+ )
64
+ end
65
+ let (:song) { OpenStruct.new(:title => "Downtown") }
66
+ let (:songs) { [song=OpenStruct.new(:title => "Calling"), song] }
67
+ let (:form) { AlbumForm.new(album) }
68
+
69
+
70
+ describe "incorrect #validate" do
71
+ before { form.validate(
72
+ "hit" =>{"title" => ""},
73
+ "title" => "",
74
+ "songs" => [{"title" => ""}, {"title" => ""}]) } # FIXME: what happens if item is missing?
75
+
76
+ it do
77
+ form.errors.messages.must_equal({
78
+ :title => ["must be filled"],
79
+ :"hit.title"=>["must be filled"],
80
+ :"songs.title"=>["must be filled"],
81
+ :"band.label.name"=>["is missing"]
82
+ })
83
+ end
84
+
85
+ it do
86
+ #form.errors.must_equal({:title => ["must be filled"]})
87
+ # TODO: this should only contain local errors?
88
+ end
89
+
90
+ # nested forms keep their own Errors:
91
+ it { form.hit.errors.messages.must_equal({:title=>["must be filled"]}) }
92
+ it { form.songs[0].errors.messages.must_equal({:title=>["must be filled"]}) }
93
+
94
+ it do
95
+ form.errors.messages.must_equal({
96
+ :title => ["must be filled"],
97
+ :"hit.title" => ["must be filled"],
98
+ :"songs.title"=> ["must be filled"],
99
+ :"band.label.name"=>["is missing"]
100
+ })
101
+ end
102
+ end
103
+
104
+
105
+ describe "#validate with main form invalid" do
106
+ it do
107
+ form.validate("title"=>"", "band"=>{"label"=>{:name => "Fat Wreck"}}).must_equal false
108
+ form.errors.messages.must_equal({:title=>["must be filled"]})
109
+ end
110
+ end
111
+
112
+
113
+ describe "#validate with middle nested form invalid" do
114
+ before { @result = form.validate("hit"=>{"title" => ""}, "band"=>{"label"=>{:name => "Fat Wreck"}}) }
115
+
116
+ it { @result.must_equal false }
117
+ it { form.errors.messages.must_equal({:"hit.title"=>["must be filled"]}) }
118
+ end
119
+
120
+
121
+ describe "#validate with collection form invalid" do
122
+ before { @result = form.validate("songs"=>[{"title" => ""}], "band"=>{"label"=>{:name => "Fat Wreck"}}) }
123
+
124
+ it { @result.must_equal false }
125
+ it { form.errors.messages.must_equal({:"songs.title"=>["must be filled"]}) }
126
+ end
127
+
128
+
129
+ describe "#validate with collection and 2-level-nested invalid" do
130
+ before { @result = form.validate("songs"=>[{"title" => ""}], "band" => {"label" => {}}) }
131
+
132
+ it { @result.must_equal false }
133
+ it { form.errors.messages.must_equal({:"songs.title"=>["must be filled"], :"band.label.name"=>["is missing"]}) }
134
+ end
135
+
136
+ describe "#validate with nested form using :base invalid" do
137
+ it do
138
+ result = form.validate("songs"=>[{"title" => "Someday"}], "band" => {"name" => "Nickelback", "label" => {"name" => "Roadrunner Records"}})
139
+ result.must_equal false
140
+ form.errors.messages.must_equal({:"band.name"=>["You are a bad person"]})
141
+ end
142
+ end
143
+
144
+ describe "correct #validate" do
145
+ before { @result = form.validate(
146
+ "hit" => {"title" => "Sacrifice"},
147
+ "title" => "Second Heat",
148
+ "songs" => [{"title"=>"Heart Of A Lion"}],
149
+ "band" => {"label"=>{:name => "Fat Wreck"}}
150
+ ) }
151
+
152
+ it { @result.must_equal true }
153
+ it { form.hit.title.must_equal "Sacrifice" }
154
+ it { form.title.must_equal "Second Heat" }
155
+ it { form.songs.first.title.must_equal "Heart Of A Lion" }
156
+ end
157
+
158
+
159
+ describe "Errors#to_s" do
160
+ before { form.validate("songs"=>[{"title" => ""}], "band" => {"label" => {}}) }
161
+
162
+ # to_s is aliased to messages
163
+ it { form.errors.to_s.must_equal "{:\"songs.title\"=>[\"must be filled\"], :\"band.label.name\"=>[\"is missing\"]}" }
164
+ end
165
+ end
@@ -0,0 +1,65 @@
1
+ require 'test_helper'
2
+
3
+ class FeatureInheritanceTest < BaseTest
4
+ Song = Struct.new(:title, :album, :composer)
5
+ Album = Struct.new(:name, :songs, :artist)
6
+ Artist = Struct.new(:name)
7
+
8
+ module Date
9
+ def date
10
+ "May 16"
11
+ end
12
+
13
+ def self.included(includer)
14
+ includer.send :register_feature, self
15
+ end
16
+ end
17
+
18
+ # module Name
19
+ # def name
20
+ # "Violins"
21
+ # end
22
+ # end
23
+
24
+ class AlbumForm < Reform::Form
25
+ feature Date # feature.
26
+ property :name
27
+
28
+ collection :songs do
29
+ property :title
30
+
31
+ property :composer do
32
+ property :name
33
+ end
34
+ end
35
+
36
+ property :artist do
37
+ property :name
38
+ end
39
+ end
40
+
41
+ let (:song) { Song.new("Broken") }
42
+ let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
43
+ let (:composer) { Artist.new("Greg Graffin") }
44
+ let (:artist) { Artist.new("Bad Religion") }
45
+ let (:album) { Album.new("The Dissent Of Man", [song, song_with_composer], artist) }
46
+
47
+ let (:form) { AlbumForm.new(album) }
48
+
49
+ it do
50
+ form.date.must_equal "May 16"
51
+ form.songs[0].date.must_equal "May 16"
52
+ end
53
+
54
+ # it { subject.class.include?(Reform::Form::ActiveModel) }
55
+ # it { subject.class.include?(Reform::Form::Coercion) }
56
+ # it { subject.is_a?(Reform::Form::MultiParameterAttributes) }
57
+
58
+ # it { subject.band.class.include?(Reform::Form::ActiveModel) }
59
+ # it { subject.band.is_a?(Reform::Form::Coercion) }
60
+ # it { subject.band.is_a?(Reform::Form::MultiParameterAttributes) }
61
+
62
+ # it { subject.band.label.is_a?(Reform::Form::ActiveModel) }
63
+ # it { subject.band.label.is_a?(Reform::Form::Coercion) }
64
+ # it { subject.band.label.is_a?(Reform::Form::MultiParameterAttributes) }
65
+ end
@@ -0,0 +1,44 @@
1
+ array?: "%{name} must be an array"
2
+
3
+ empty?: "%{name} cannot be empty"
4
+
5
+ exclusion?: "%{name} must not be one of: %{list}"
6
+
7
+ eql?: "%{name} must be equal to %{eql_value}"
8
+
9
+ filled?: "%{name} must be filled"
10
+
11
+ format?: "%{name} is in invalid format"
12
+
13
+ gt?: "%{name} must be greater than %{num} (%{value} was given)"
14
+
15
+ gteq?: "%{name} must be greater than or equal to %{num}"
16
+
17
+ hash?: "%{name} must be a hash"
18
+
19
+ inclusion?: "%{name} must be one of: %{list}"
20
+
21
+ int?: "%{name} must be an integer"
22
+
23
+ key?: "%{name} is missing"
24
+
25
+ lt?: "%{name} must be less than %{num} (%{value} was given)"
26
+
27
+ lteq?: "%{name} must be less than or equal to %{num}"
28
+
29
+ max_size?: "%{name} size cannot be greater than %{num}"
30
+
31
+ min_size?: "%{name} size cannot be less than %{num}"
32
+
33
+ nil?: "%{name} cannot be nil"
34
+
35
+ size?:
36
+ range: "%{name} size must be within %{left} - %{right}"
37
+ default: "%{name} size must be %{num}"
38
+
39
+ str?: "%{name} must be a string"
40
+
41
+ good_musical_taste?: "you're a bad person"
42
+
43
+ form_access_validation?: "this doesn't look like a Reform form dude!!"
44
+
@@ -0,0 +1,24 @@
1
+ require 'test_helper'
2
+
3
+ class FormOptionTest < MiniTest::Spec
4
+ Song = Struct.new(:title)
5
+ Album = Struct.new(:song)
6
+
7
+ class SongForm < Reform::Form
8
+ property :title
9
+ validation do
10
+ key(:title).required
11
+ end
12
+ end
13
+
14
+ class AlbumForm < Reform::Form
15
+ property :song, form: SongForm
16
+ end
17
+
18
+ it do
19
+ form = AlbumForm.new(Album.new(Song.new("When It Comes To You")))
20
+ form.song.title.must_equal "When It Comes To You"
21
+
22
+ form.validate(song: {title: "Run For Cover"})
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ require 'test_helper'
2
+
3
+ class FormTest < MiniTest::Spec
4
+ Artist = Struct.new(:name)
5
+
6
+ class AlbumForm < Reform::Form
7
+ property :title
8
+
9
+ property :hit do
10
+ property :title
11
+ end
12
+
13
+ collection :songs do
14
+ property :title
15
+ end
16
+
17
+ property :band do # yepp, people do crazy stuff like that.
18
+ property :label do
19
+ property :name
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "::dup" do
25
+ let (:cloned) { AlbumForm.clone }
26
+
27
+ # #dup is called in Op.inheritable_attr(:contract_class), it must be subclass of the original one.
28
+ it { cloned.wont_equal AlbumForm }
29
+ it { AlbumForm.definitions.wont_equal cloned.definitions }
30
+
31
+ it do
32
+ # currently, forms need a name for validation, even without AM.
33
+ cloned.singleton_class.class_eval do
34
+ def name
35
+ "Album"
36
+ end
37
+ end
38
+
39
+ cloned.validation do
40
+ key(:title).required
41
+ end
42
+
43
+ cloned.new(OpenStruct.new).validate({})
44
+ end
45
+ end
46
+
47
+ describe "#initialize" do
48
+ class ArtistForm < Reform::Form
49
+ property :name
50
+ property :current_user, virtual: true
51
+ end
52
+
53
+ it "allows injecting :virtual options" do
54
+ ArtistForm.new(Artist.new, current_user: Object).current_user.must_equal Object
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,75 @@
1
+ require 'test_helper'
2
+
3
+ class AsTest < BaseTest
4
+ class AlbumForm < Reform::Form
5
+ property :name, from: :title
6
+
7
+ property :single, from: :hit do
8
+ property :title
9
+ end
10
+
11
+ collection :tracks, from: :songs do
12
+ property :name, from: :title
13
+ end
14
+
15
+ property :band do
16
+ property :company, from: :label do
17
+ property :business, from: :name
18
+ end
19
+ end
20
+ end
21
+
22
+ let (:song2) { Song.new("Roxanne") }
23
+
24
+ let (:params) {
25
+ {
26
+ "name" => "Best Of The Police",
27
+ "single" => {"title" => "So Lonely"},
28
+ "tracks" => [{"name" => "Message In A Bottle"}, {"name" => "Roxanne"}]
29
+ }
30
+ }
31
+
32
+ subject { AlbumForm.new(Album.new("Best Of", hit, [Song.new("Fallout"), song2])) }
33
+
34
+ it { subject.name.must_equal "Best Of" }
35
+ it { subject.single.title.must_equal "Roxanne" }
36
+ it { subject.tracks[0].name.must_equal "Fallout" }
37
+ it { subject.tracks[1].name.must_equal "Roxanne" }
38
+
39
+
40
+ describe "#validate" do
41
+
42
+
43
+ before { subject.validate(params) }
44
+
45
+ it { subject.name.must_equal "Best Of The Police" }
46
+ it { subject.single.title.must_equal "So Lonely" }
47
+ it { subject.tracks[0].name.must_equal "Message In A Bottle" }
48
+ it { subject.tracks[1].name.must_equal "Roxanne" }
49
+ end
50
+
51
+
52
+ describe "#sync" do
53
+ before {
54
+ subject.tracks[1].name = "Livin' Ain't No Crime"
55
+ subject.sync
56
+ }
57
+
58
+ it { song2.title.must_equal "Livin' Ain't No Crime" }
59
+ end
60
+
61
+
62
+ describe "#save (nested hash)" do
63
+ before { subject.validate(params) }
64
+
65
+ it do
66
+ hash = nil
67
+
68
+ subject.save do |nested_hash|
69
+ hash = nested_hash
70
+ end
71
+
72
+ hash.must_equal({"title"=>"Best Of The Police", "hit"=>{"title"=>"So Lonely"}, "songs"=>[{"title"=>"Message In A Bottle"}, {"title"=>"Roxanne"}]})
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,119 @@
1
+ require 'test_helper'
2
+ require 'representable/json'
3
+
4
+ class InheritTest < BaseTest
5
+ Populator = Reform::Form::Populator
6
+
7
+ class AlbumForm < Reform::Form
8
+ property :title, deserializer: {instance: "Instance"}, skip_if: "skip_if in AlbumForm" # allow direct configuration of :deserializer.
9
+
10
+ property :hit, populator: "Populator" do
11
+ property :title
12
+ end
13
+
14
+ collection :songs, populate_if_empty: lambda {}, skip_if: :all_blank do
15
+ property :title
16
+ end
17
+
18
+ property :artist, populate_if_empty: lambda {} do
19
+
20
+ def artist_id
21
+ 1
22
+ end
23
+ end
24
+ end
25
+
26
+ puts
27
+ puts "inherit"
28
+
29
+ class CompilationForm < AlbumForm
30
+ property :title, inherit: true, skip_if: "skip_if from CompilationForm"
31
+ puts "[#{options_for(:title)[:deserializer].object_id}] COM@@@@@ #{options_for(:title)[:deserializer].inspect}"
32
+ # property :hit, :inherit => true do
33
+ # property :rating
34
+ # validates :title, :rating, :presence => true
35
+ # end
36
+
37
+ # puts representer_class.representable_attrs.
38
+ # get(:hit)[:extend].evaluate(nil).new(OpenStruct.new).rating
39
+
40
+ # NO collection here, this is entirely inherited.
41
+ # collection :songs, ..
42
+
43
+ property :artist, inherit: true do # inherit everything, but explicitely.
44
+ end
45
+
46
+ # completely override.
47
+ property :hit, skip_if: "SkipParse" do
48
+ end
49
+
50
+ # override partly.
51
+ end
52
+
53
+ let (:album) { Album.new(nil, OpenStruct.new(:hit => OpenStruct.new()) ) }
54
+ subject { CompilationForm.new(album) }
55
+
56
+
57
+ # valid.
58
+ # it {
59
+ # subject.validate("hit" => {"title" => "LA Drone", "rating" => 10})
60
+ # subject.hit.title.must_equal "LA Drone"
61
+ # subject.hit.rating.must_equal 10
62
+ # subject.errors.messages.must_equal({})
63
+ # }
64
+
65
+ # it do
66
+ # subject.validate({})
67
+ # subject.hit.title.must_equal nil
68
+ # subject.hit.rating.must_equal nil
69
+ # subject.errors.messages.must_equal({:"hit.title"=>["can't be blank"], :"hit.rating"=>["can't be blank"]})
70
+ # end
71
+
72
+ require "pp"
73
+
74
+ it "xxx" do
75
+ # sub hashes like :deserializer must be properly cloned when inheriting.
76
+ AlbumForm.options_for(:title)[:deserializer].object_id.wont_equal CompilationForm.options_for(:title)[:deserializer].object_id
77
+
78
+ # don't overwrite direct deserializer: {} configuration.
79
+ AlbumForm.options_for(:title)[:internal_populator].must_be_instance_of Reform::Form::Populator::Sync
80
+ AlbumForm.options_for(:title)[:deserializer][:skip_parse].must_equal "skip_if in AlbumForm"
81
+
82
+ AlbumForm.options_for(:hit)[:internal_populator].inspect.must_match /Reform::Form::Populator:.+ @user_proc="Populator"/
83
+ # AlbumForm.options_for(:hit)[:deserializer][:instance].inspect.must_be_instance_with Reform::Form::Populator, user_proc: "Populator"
84
+
85
+
86
+ AlbumForm.options_for(:songs)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
87
+ AlbumForm.options_for(:songs)[:deserializer][:skip_parse].must_be_instance_of Reform::Form::Validate::Skip::AllBlank
88
+
89
+ AlbumForm.options_for(:artist)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
90
+
91
+
92
+
93
+ CompilationForm.options_for(:title)[:deserializer][:skip_parse].must_equal "skip_if from CompilationForm"
94
+ # pp CompilationForm.options_for(:songs)
95
+ CompilationForm.options_for(:songs)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
96
+
97
+
98
+ CompilationForm.options_for(:artist)[:internal_populator].must_be_instance_of Reform::Form::Populator::IfEmpty
99
+
100
+ # completely overwrite inherited.
101
+ CompilationForm.options_for(:hit)[:internal_populator].must_be_instance_of Reform::Form::Populator::Sync # reset to default.
102
+ CompilationForm.options_for(:hit)[:deserializer][:skip_parse].must_equal "SkipParse"
103
+
104
+
105
+ # inherit: true with block will still inherit the original class.
106
+ AlbumForm.new(OpenStruct.new(artist: OpenStruct.new)).artist.artist_id.must_equal 1
107
+ CompilationForm.new(OpenStruct.new(artist: OpenStruct.new)).artist.artist_id.must_equal 1
108
+ end
109
+
110
+
111
+ class CDForm < AlbumForm
112
+ # override :artist's original populate_if_empty but with :inherit.
113
+ property :artist, inherit: true, populator: "CD Populator" do
114
+
115
+ end
116
+ end
117
+
118
+ it { CDForm.options_for(:artist)[:internal_populator].instance_variable_get(:@user_proc).must_equal "CD Populator" }
119
+ end