reform 2.3.0.rc1 → 2.5.0

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 (65) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +5 -1
  3. data/.travis.yml +7 -11
  4. data/CHANGES.md +43 -3
  5. data/Gemfile +2 -5
  6. data/ISSUE_TEMPLATE.md +1 -1
  7. data/LICENSE.txt +1 -1
  8. data/README.md +7 -9
  9. data/Rakefile +6 -10
  10. data/lib/reform/contract.rb +7 -7
  11. data/lib/reform/contract/custom_error.rb +41 -0
  12. data/lib/reform/contract/validate.rb +10 -6
  13. data/lib/reform/errors.rb +27 -15
  14. data/lib/reform/form.rb +22 -11
  15. data/lib/reform/form/call.rb +1 -1
  16. data/lib/reform/form/composition.rb +2 -2
  17. data/lib/reform/form/dry.rb +22 -60
  18. data/lib/reform/form/dry/input_hash.rb +37 -0
  19. data/lib/reform/form/populator.rb +9 -11
  20. data/lib/reform/form/prepopulate.rb +3 -2
  21. data/lib/reform/form/validate.rb +19 -12
  22. data/lib/reform/result.rb +36 -9
  23. data/lib/reform/validation.rb +10 -8
  24. data/lib/reform/validation/groups.rb +2 -4
  25. data/lib/reform/version.rb +1 -1
  26. data/reform.gemspec +9 -9
  27. data/test/benchmarking.rb +10 -11
  28. data/test/call_test.rb +8 -8
  29. data/test/changed_test.rb +13 -13
  30. data/test/coercion_test.rb +56 -24
  31. data/test/composition_test.rb +49 -51
  32. data/test/contract/custom_error_test.rb +55 -0
  33. data/test/contract_test.rb +18 -18
  34. data/test/default_test.rb +3 -3
  35. data/test/deserialize_test.rb +14 -17
  36. data/test/docs/validation_test.rb +134 -0
  37. data/test/errors_test.rb +131 -86
  38. data/test/feature_test.rb +9 -11
  39. data/test/fixtures/dry_error_messages.yml +65 -52
  40. data/test/form_option_test.rb +3 -3
  41. data/test/form_test.rb +6 -6
  42. data/test/from_test.rb +17 -21
  43. data/test/inherit_test.rb +28 -35
  44. data/test/module_test.rb +23 -28
  45. data/test/parse_option_test.rb +12 -12
  46. data/test/parse_pipeline_test.rb +3 -3
  47. data/test/populate_test.rb +146 -93
  48. data/test/populator_skip_test.rb +3 -4
  49. data/test/prepopulator_test.rb +20 -21
  50. data/test/read_only_test.rb +12 -1
  51. data/test/readable_test.rb +7 -7
  52. data/test/reform_test.rb +38 -42
  53. data/test/save_test.rb +16 -19
  54. data/test/setup_test.rb +15 -15
  55. data/test/skip_if_test.rb +30 -19
  56. data/test/skip_setter_and_getter_test.rb +8 -9
  57. data/test/test_helper.rb +12 -5
  58. data/test/validate_test.rb +160 -140
  59. data/test/validation/dry_validation_test.rb +407 -236
  60. data/test/validation/result_test.rb +29 -31
  61. data/test/validation_library_provided_test.rb +3 -3
  62. data/test/virtual_test.rb +46 -6
  63. data/test/writeable_test.rb +13 -13
  64. metadata +32 -29
  65. data/test/readonly_test.rb +0 -14
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "2.3.0.rc1"
2
+ VERSION = "2.5.0".freeze
3
3
  end
data/reform.gemspec CHANGED
@@ -1,30 +1,30 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path("lib", __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'reform/version'
3
+ require "reform/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "reform"
7
7
  spec.version = Reform::VERSION
8
8
  spec.authors = ["Nick Sutterer", "Fran Worley"]
9
9
  spec.email = ["apotonick@gmail.com", "frances@safetytoolbox.co.uk"]
10
- spec.description = %q{Form object decoupled from models.}
11
- spec.summary = %q{Form object decoupled from models with validation, population and presentation.}
10
+ spec.description = "Form object decoupled from models."
11
+ spec.summary = "Form object decoupled from models with validation, population and presentation."
12
12
  spec.homepage = "https://github.com/trailblazer/reform"
13
13
  spec.license = "MIT"
14
14
 
15
15
  spec.files = `git ls-files`.split($/)
16
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.executables = spec.files.grep(%r(^bin/)) { |f| File.basename(f) }
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency "disposable", ">= 0.4.2", "< 0.5.0"
21
- spec.add_dependency "uber", "< 0.2.0"
22
21
  spec.add_dependency "representable", ">= 2.4.0", "< 3.1.0"
22
+ spec.add_dependency "uber", "< 0.2.0"
23
23
 
24
24
  spec.add_development_dependency "bundler"
25
- spec.add_development_dependency "rake"
26
25
  spec.add_development_dependency "minitest"
27
- spec.add_development_dependency "dry-types"
26
+ spec.add_development_dependency "minitest-line"
27
+ spec.add_development_dependency "pry-byebug"
28
28
  spec.add_development_dependency "multi_json"
29
- spec.add_development_dependency "dry-validation", ">= 0.10.1"
29
+ spec.add_development_dependency "rake"
30
30
  end
data/test/benchmarking.rb CHANGED
@@ -1,29 +1,29 @@
1
- require 'reform'
2
- require 'benchmark/ips'
1
+ require "reform"
2
+ require "benchmark/ips"
3
3
  require "reform/form/dry"
4
4
 
5
5
  class BandForm < Reform::Form
6
6
  feature Reform::Form::Dry
7
- property :name#, validates: {presence: true}
7
+ property :name #, validates: {presence: true}
8
8
  collection :songs do
9
- property :title#, validates: {presence: true}
9
+ property :title #, validates: {presence: true}
10
10
  end
11
11
  end
12
12
 
13
13
  class OptimizedBandForm < Reform::Form
14
14
  feature Reform::Form::Dry
15
- property :name#, validates: {presence: true}
15
+ property :name #, validates: {presence: true}
16
16
  collection :songs do
17
- property :title#, validates: {presence: true}
18
-
17
+ property :title #, validates: {presence: true}
18
+
19
19
  def deserializer(*args)
20
- # DISCUSS: should we simply delegate to class and sort out memoizing there?
20
+ # DISCUSS: should we simply delegate to class and sort out memoizing there?
21
21
  self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
22
- end
22
+ end
23
23
  end
24
24
 
25
25
  def deserializer(*args)
26
- # DISCUSS: should we simply delegate to class and sort out memoizing there?
26
+ # DISCUSS: should we simply delegate to class and sort out memoizing there?
27
27
  self.class.deserializer_class || self.class.deserializer_class = deserializer!(*args)
28
28
  end
29
29
  end
@@ -43,7 +43,6 @@ end
43
43
 
44
44
  exit
45
45
 
46
-
47
46
  songs = 50.times.collect { OpenStruct.new(title: "Be Stag") }
48
47
  band = OpenStruct.new(name: "Teenage Bottlerock", songs: songs)
49
48
 
data/test/call_test.rb CHANGED
@@ -7,17 +7,17 @@ class CallTest < Minitest::Spec
7
7
  property :title
8
8
 
9
9
  validation do
10
- key(:title).required
10
+ params { required(:title).filled }
11
11
  end
12
12
  end
13
13
 
14
- let (:form) { SongForm.new(Song.new) }
14
+ let(:form) { SongForm.new(Song.new) }
15
15
 
16
- it { form.(title: "True North").success?.must_equal true }
17
- it { form.(title: "True North").failure?.must_equal false }
18
- it { form.(title: "").success?.must_equal false }
19
- it { form.(title: "").failure?.must_equal true }
16
+ it { assert form.(title: "True North").success? }
17
+ it { refute form.(title: "True North").failure? }
18
+ it { refute form.(title: "").success? }
19
+ it { assert form.(title: "").failure? }
20
20
 
21
- it { form.(title: "True North").errors.messages.must_equal({}) }
22
- it { form.(title: "").errors.messages.must_equal({:title=>["must be filled"]}) }
21
+ it { assert_equal form.(title: "True North").errors.messages, {} }
22
+ it { assert_equal form.(title: "").errors.messages, title: ["must be filled"] }
23
23
  end
data/test/changed_test.rb CHANGED
@@ -1,5 +1,5 @@
1
- require 'test_helper'
2
- require 'reform/form/coercion'
1
+ require "test_helper"
2
+ require "reform/form/coercion"
3
3
 
4
4
  class ChangedTest < MiniTest::Spec
5
5
  Song = Struct.new(:title, :album, :composer)
@@ -18,24 +18,24 @@ class ChangedTest < MiniTest::Spec
18
18
  end
19
19
  end
20
20
 
21
- let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
22
- let (:composer) { Artist.new("Greg Graffin") }
23
- let (:album) { Album.new("The Dissent Of Man", [song_with_composer]) }
21
+ let(:song_with_composer) { Song.new("Resist Stance", nil, composer) }
22
+ let(:composer) { Artist.new("Greg Graffin") }
23
+ let(:album) { Album.new("The Dissent Of Man", [song_with_composer]) }
24
24
 
25
- let (:form) { AlbumForm.new(album) }
25
+ let(:form) { AlbumForm.new(album) }
26
26
 
27
27
  # nothing changed after setup.
28
28
  it do
29
- form.changed?(:name).must_equal false
30
- form.songs[0].changed?(:title).must_equal false
31
- form.songs[0].composer.changed?(:name).must_equal false
29
+ refute form.changed?(:name)
30
+ refute form.songs[0].changed?(:title)
31
+ refute form.songs[0].composer.changed?(:name)
32
32
  end
33
33
 
34
34
  # after validate, things might have changed.
35
35
  it do
36
36
  form.validate("name" => "Out Of Bounds", "songs" => [{"composer" => {"name" => "Ingemar Jansson & Mikael Danielsson"}}])
37
- form.changed?(:name).must_equal true
38
- form.songs[0].changed?(:title).must_equal false
39
- form.songs[0].composer.changed?(:name).must_equal true
37
+ assert form.changed?(:name)
38
+ refute form.songs[0].changed?(:title)
39
+ assert form.songs[0].composer.changed?(:name)
40
40
  end
41
- end
41
+ end
@@ -1,21 +1,23 @@
1
1
  require "test_helper"
2
2
  require "reform/form/coercion"
3
+ require "disposable/twin/property/hash"
3
4
 
4
5
  class CoercionTest < BaseTest
5
6
  class Irreversible
6
7
  def self.call(value)
7
- value*2
8
+ value * 2
8
9
  end
9
10
  end
10
11
 
11
12
  class Form < TestForm
12
13
  feature Coercion
14
+ include Disposable::Twin::Property::Hash
13
15
 
14
- property :released_at, type: Types::Form::DateTime
16
+ property :released_at, type: Types::Params::DateTime
15
17
 
16
18
  property :hit do
17
- property :length, type: Types::Form::Int
18
- property :good, type: Types::Form::Bool
19
+ property :length, type: Types::Params::Integer
20
+ property :good, type: Types::Params::Bool
19
21
  end
20
22
 
21
23
  property :band do
@@ -23,44 +25,74 @@ class CoercionTest < BaseTest
23
25
  property :value, type: Irreversible
24
26
  end
25
27
  end
28
+
29
+ property :metadata, field: :hash do
30
+ property :publication_settings do
31
+ property :featured, type: Types::Params::Bool
32
+ end
33
+ end
26
34
  end
27
35
 
28
36
  subject do
29
37
  Form.new(album)
30
38
  end
31
39
 
32
- let (:album) {
40
+ let(:album) do
33
41
  OpenStruct.new(
34
- :released_at => "31/03/1981",
35
- :hit => OpenStruct.new(:length => "312"),
36
- :band => Band.new(OpenStruct.new(:value => "9999.99"))
42
+ released_at: "31/03/1981",
43
+ hit: OpenStruct.new(length: "312"),
44
+ band: Band.new(OpenStruct.new(value: "9999.99")),
45
+ metadata: {}
37
46
  )
38
- }
47
+ end
39
48
 
40
49
  # 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
-
50
+ it { assert_equal subject.released_at, "31/03/1981" } # NO coercion in setup.
51
+ it { assert_equal subject.hit.length, "312" }
52
+ it { assert_equal subject.band.label.value, "9999.99" }
45
53
 
46
- let (:params) {
54
+ let(:params) do
47
55
  {
48
- :released_at => "30/03/1981",
49
- :hit => {:length => "312"},
50
- :band => {:label => {:value => "9999.99"}}
56
+ released_at: "30/03/1981",
57
+ hit: {
58
+ length: "312",
59
+ good: "0",
60
+ },
61
+ band: {
62
+ label: {
63
+ value: "9999.99"
64
+ }
65
+ },
66
+ metadata: {
67
+ publication_settings: {
68
+ featured: "0"
69
+ }
70
+ }
51
71
  }
52
- }
53
-
72
+ end
54
73
 
55
74
  # validate
56
75
  describe "#validate" do
57
76
  before { subject.validate(params) }
58
77
 
59
- it { subject.released_at.must_equal DateTime.parse("30/03/1981") }
60
- it { subject.hit.length.must_equal 312 }
61
- it { assert_nil subject.hit.good }
62
- it { subject.band.label.value.must_equal "9999.999999.99" } # coercion happened once.
78
+ it { assert_equal subject.released_at, DateTime.parse("30/03/1981") }
79
+ it { assert_equal subject.hit.length, 312 }
80
+ it { assert_equal subject.hit.good, false }
81
+ it { assert_equal subject.band.label.value, "9999.999999.99" } # coercion happened once.
82
+ it { assert_equal subject.metadata.publication_settings.featured, false }
63
83
  end
64
84
 
65
- # save
85
+ # sync
86
+ describe "#sync" do
87
+ before do
88
+ assert subject.validate(params)
89
+ subject.sync
90
+ end
91
+
92
+ it { assert_equal album.released_at, DateTime.parse("30/03/1981") }
93
+ it { assert_equal album.hit.length, 312 }
94
+ it { assert_equal album.hit.good, false }
95
+ it { assert_nil album.metadata[:publication_settings] }
96
+ it { assert_equal album.metadata["publication_settings"]["featured"], false }
97
+ end
66
98
  end
@@ -1,4 +1,4 @@
1
- require 'test_helper'
1
+ require "test_helper"
2
2
 
3
3
  class FormCompositionInheritanceTest < MiniTest::Spec
4
4
  module SizePrice
@@ -26,16 +26,15 @@ class FormCompositionInheritanceTest < MiniTest::Spec
26
26
  property :size, inherit: true, on: :measurement
27
27
  end
28
28
 
29
-
30
- let (:measurement) { Measurement.new(:l) }
31
- let (:tshirt) { Tshirt.new(2, :m) }
32
- let (:form) { OutfitForm.new(tshirt: tshirt, measurement: measurement) }
29
+ let(:measurement) { Measurement.new(:l) }
30
+ let(:tshirt) { Tshirt.new(2, :m) }
31
+ let(:form) { OutfitForm.new(tshirt: tshirt, measurement: measurement) }
33
32
 
34
33
  Tshirt = Struct.new(:price, :size)
35
34
  Measurement = Struct.new(:size)
36
35
 
37
- it { form.price.must_equal 6 }
38
- it { form.price(for_size: :s).must_equal 2 }
36
+ it { assert_equal form.price, 6 }
37
+ it { assert_equal form.price(for_size: :s), 2 }
39
38
  end
40
39
 
41
40
  class FormCompositionTest < MiniTest::Spec
@@ -46,49 +45,50 @@ class FormCompositionTest < MiniTest::Spec
46
45
  class RequestForm < TestForm
47
46
  include Composition
48
47
 
49
- property :name, :on => :requester
50
- property :requester_id, :on => :requester, :from => :id
51
- properties :title, :id, :on => :song
48
+ property :name, on: :requester
49
+ property :requester_id, on: :requester, from: :id
50
+ properties :title, :id, on: :song
52
51
  # property :channel # FIXME: what about the "main model"?
53
- property :channel, :virtual => true, :on => :song
54
- property :requester, :on => :requester
55
- property :captcha, :on => :song, :virtual => true
52
+ property :channel, virtual: true, on: :song
53
+ property :requester, on: :requester
54
+ property :captcha, on: :song, virtual: true
56
55
 
57
56
  validation do
58
- required(:name).filled
59
- required(:title).filled
57
+ params do
58
+ required(:name).filled
59
+ required(:title).filled
60
+ end
60
61
  end
61
62
 
62
- property :band, :on => :song do
63
+ property :band, on: :song do
63
64
  property :title
64
65
  end
65
66
  end
66
67
 
67
- let (:form) { RequestForm.new(:song => song, :requester => requester) }
68
- let (:song) { Song.new(1, "Rio", band) }
69
- let (:requester) { Requester.new(2, "Duran Duran", "MCP") }
70
- let (:band) { Band.new("Duran^2") }
68
+ let(:form) { RequestForm.new(song: song, requester: requester) }
69
+ let(:song) { Song.new(1, "Rio", band) }
70
+ let(:requester) { Requester.new(2, "Duran Duran", "MCP") }
71
+ let(:band) { Band.new("Duran^2") }
71
72
 
72
73
  # delegation form -> composition works
73
- it { form.id.must_equal 1 }
74
- it { form.title.must_equal "Rio" }
75
- it { form.name.must_equal "Duran Duran" }
76
- it { form.requester_id.must_equal 2 }
74
+ it { assert_equal form.id, 1 }
75
+ it { assert_equal form.title, "Rio" }
76
+ it { assert_equal form.name, "Duran Duran" }
77
+ it { assert_equal form.requester_id, 2 }
77
78
  it { assert_nil form.channel }
78
- it { form.requester.must_equal "MCP" } # same name as composed model.
79
+ it { assert_equal form.requester, "MCP" } # same name as composed model.
79
80
  it { assert_nil form.captcha }
80
81
 
81
82
  # #model just returns <Composition>.
82
- it { form.mapper.must_be_kind_of Disposable::Composition }
83
+ it { assert form.mapper.is_a? Disposable::Composition }
83
84
 
84
85
  # #model[] -> composed models
85
- it { form.model[:requester].must_equal requester }
86
- it { form.model[:song].must_equal song }
87
-
86
+ it { assert_equal form.model[:requester], requester }
87
+ it { assert_equal form.model[:song], song }
88
88
 
89
89
  it "creates Composition for you" do
90
- form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal true
91
- form.validate("title" => "", "name" => "Frenzal Rhomb").must_equal false
90
+ assert_equal form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb"), true
91
+ assert_equal form.validate("title" => "", "name" => "Frenzal Rhomb"), false
92
92
  end
93
93
 
94
94
  describe "#save" do
@@ -101,7 +101,7 @@ class FormCompositionTest < MiniTest::Spec
101
101
  hash[:title] = form.title
102
102
  end
103
103
 
104
- hash.must_equal({:name=>"Duran Duran", :title=>"Rio"})
104
+ assert_equal hash, name: "Duran Duran", title: "Rio"
105
105
  end
106
106
 
107
107
  it "provides nested symbolized hash as second block argument" do
@@ -113,11 +113,10 @@ class FormCompositionTest < MiniTest::Spec
113
113
  hash = map
114
114
  end
115
115
 
116
- hash.must_equal({
117
- :song=>{"title"=>"Greyhound", "id"=>1, "channel" => "JJJ", "captcha"=>"wonderful", "band"=>{"title"=>"Duran^2"}},
118
- :requester=>{"name"=>"Frenzal Rhomb", "id"=>2, "requester" => "MCP"}
119
- }
120
- )
116
+ assert_equal hash, {
117
+ song: {"title" => "Greyhound", "id" => 1, "channel" => "JJJ", "captcha" => "wonderful", "band" => {"title" => "Duran^2"}},
118
+ requester: {"name" => "Frenzal Rhomb", "id" => 2, "requester" => "MCP"}
119
+ }
121
120
  end
122
121
 
123
122
  it "xxx pushes data to models and calls #save when no block passed" do
@@ -126,16 +125,16 @@ class FormCompositionTest < MiniTest::Spec
126
125
  band.extend(Saveable)
127
126
 
128
127
  form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
129
- form.captcha.must_equal "1337" # TODO: move to separate test.
128
+ assert_equal form.captcha, "1337" # TODO: move to separate test.
130
129
 
131
130
  form.save
132
131
 
133
- requester.name.must_equal "Frenzal Rhomb"
134
- requester.saved?.must_equal true
135
- song.title.must_equal "Greyhound"
136
- song.saved?.must_equal true
137
- song.band.title.must_equal "Duran^2"
138
- song.band.saved?.must_equal true
132
+ assert_equal requester.name, "Frenzal Rhomb"
133
+ assert_equal requester.saved?, true
134
+ assert_equal song.title, "Greyhound"
135
+ assert_equal song.saved?, true
136
+ assert_equal song.band.title, "Duran^2"
137
+ assert_equal song.band.saved?, true
139
138
  end
140
139
 
141
140
  it "returns true when models all save successfully" do
@@ -143,7 +142,7 @@ class FormCompositionTest < MiniTest::Spec
143
142
  requester.extend(Saveable)
144
143
  band.extend(Saveable)
145
144
 
146
- form.save.must_equal true
145
+ assert_equal form.save, true
147
146
  end
148
147
 
149
148
  it "returns false when one or more models don't save successfully" do
@@ -157,17 +156,16 @@ class FormCompositionTest < MiniTest::Spec
157
156
  requester.extend(Saveable)
158
157
  band.extend(Saveable)
159
158
 
160
- form.save.must_equal false
159
+ assert_equal form.save, false
161
160
  end
162
161
  end
163
162
  end
164
163
 
165
-
166
164
  class FormCompositionCollectionTest < MiniTest::Spec
167
165
  Book = Struct.new(:id, :name)
168
166
  Library = Struct.new(:id) do
169
167
  def books
170
- [Book.new(1,"My book")]
168
+ [Book.new(1, "My book")]
171
169
  end
172
170
  end
173
171
 
@@ -180,8 +178,8 @@ class FormCompositionCollectionTest < MiniTest::Spec
180
178
  end
181
179
  end
182
180
 
183
- let (:form) { LibraryForm.new(library: library) }
184
- let (:library) { Library.new(2) }
181
+ let(:form) { LibraryForm.new(library: library) }
182
+ let(:library) { Library.new(2) }
185
183
 
186
- it { form.save do |hash| hash.must_equal({:library=>{"books"=>[{"id"=>1, "name"=>"My book"}]}}) end }
184
+ it { form.save { |hash| assert_equal hash, { library: { "books" => [{ "id" => 1, "name" => "My book" }] } } } }
187
185
  end