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.
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