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