reform 2.2.4 → 2.3.3

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 (103) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +5 -1
  3. data/.travis.yml +11 -6
  4. data/Appraisals +8 -0
  5. data/CHANGES.md +57 -4
  6. data/CONTRIBUTING.md +31 -0
  7. data/Gemfile +2 -16
  8. data/ISSUE_TEMPLATE.md +25 -0
  9. data/LICENSE.txt +1 -1
  10. data/README.md +5 -7
  11. data/Rakefile +16 -9
  12. data/gemfiles/0.13.0.gemfile +8 -0
  13. data/gemfiles/1.5.0.gemfile +9 -0
  14. data/lib/reform.rb +1 -0
  15. data/lib/reform/contract.rb +7 -17
  16. data/lib/reform/contract/custom_error.rb +41 -0
  17. data/lib/reform/contract/validate.rb +53 -23
  18. data/lib/reform/errors.rb +61 -0
  19. data/lib/reform/form.rb +36 -10
  20. data/lib/reform/form/call.rb +1 -1
  21. data/lib/reform/form/composition.rb +2 -2
  22. data/lib/reform/form/dry.rb +10 -58
  23. data/lib/reform/form/dry/input_hash.rb +37 -0
  24. data/lib/reform/form/dry/new_api.rb +45 -0
  25. data/lib/reform/form/dry/old_api.rb +61 -0
  26. data/lib/reform/form/populator.rb +11 -27
  27. data/lib/reform/form/prepopulate.rb +4 -3
  28. data/lib/reform/form/validate.rb +28 -13
  29. data/lib/reform/result.rb +90 -0
  30. data/lib/reform/validation.rb +19 -11
  31. data/lib/reform/validation/groups.rb +12 -27
  32. data/lib/reform/version.rb +1 -1
  33. data/reform.gemspec +14 -13
  34. data/test/benchmarking.rb +39 -6
  35. data/test/call_new_api.rb +23 -0
  36. data/test/call_old_api.rb +23 -0
  37. data/test/changed_test.rb +14 -14
  38. data/test/coercion_test.rb +57 -25
  39. data/test/composition_new_api.rb +186 -0
  40. data/test/composition_old_api.rb +184 -0
  41. data/test/contract/custom_error_test.rb +55 -0
  42. data/test/contract_new_api.rb +77 -0
  43. data/test/contract_old_api.rb +77 -0
  44. data/test/default_test.rb +4 -4
  45. data/test/deserialize_test.rb +17 -20
  46. data/test/errors_new_api.rb +225 -0
  47. data/test/errors_old_api.rb +230 -0
  48. data/test/feature_test.rb +10 -12
  49. data/test/fixtures/dry_error_messages.yml +73 -23
  50. data/test/fixtures/dry_new_api_error_messages.yml +104 -0
  51. data/test/form_new_api.rb +57 -0
  52. data/test/{form_test.rb → form_old_api.rb} +8 -8
  53. data/test/form_option_new_api.rb +24 -0
  54. data/test/{form_option_test.rb → form_option_old_api.rb} +5 -5
  55. data/test/from_test.rb +18 -22
  56. data/test/inherit_new_api.rb +105 -0
  57. data/test/inherit_old_api.rb +105 -0
  58. data/test/{module_test.rb → module_new_api.rb} +26 -31
  59. data/test/module_old_api.rb +146 -0
  60. data/test/parse_option_test.rb +40 -0
  61. data/test/parse_pipeline_test.rb +4 -4
  62. data/test/populate_new_api.rb +304 -0
  63. data/test/populate_old_api.rb +304 -0
  64. data/test/populator_skip_test.rb +11 -11
  65. data/test/prepopulator_test.rb +23 -24
  66. data/test/read_only_test.rb +12 -1
  67. data/test/readable_test.rb +9 -9
  68. data/test/reform_new_api.rb +204 -0
  69. data/test/{reform_test.rb → reform_old_api.rb} +44 -65
  70. data/test/save_new_api.rb +101 -0
  71. data/test/save_old_api.rb +101 -0
  72. data/test/setup_test.rb +17 -17
  73. data/test/skip_if_new_api.rb +85 -0
  74. data/test/skip_if_old_api.rb +92 -0
  75. data/test/skip_setter_and_getter_test.rb +9 -10
  76. data/test/test_helper.rb +25 -14
  77. data/test/validate_new_api.rb +453 -0
  78. data/test/{validate_test.rb → validate_old_api.rb} +121 -131
  79. data/test/validation/dry_validation_new_api.rb +835 -0
  80. data/test/validation/dry_validation_old_api.rb +772 -0
  81. data/test/validation/result_test.rb +77 -0
  82. data/test/validation_library_provided_test.rb +16 -0
  83. data/test/virtual_test.rb +47 -7
  84. data/test/writeable_test.rb +38 -9
  85. metadata +111 -56
  86. data/gemfiles/Gemfile.disposable-0.3 +0 -6
  87. data/lib/reform/contract/errors.rb +0 -43
  88. data/lib/reform/form/mongoid.rb +0 -37
  89. data/lib/reform/form/orm.rb +0 -26
  90. data/lib/reform/mongoid.rb +0 -4
  91. data/test/call_test.rb +0 -23
  92. data/test/composition_test.rb +0 -149
  93. data/test/contract_test.rb +0 -77
  94. data/test/deprecation_test.rb +0 -27
  95. data/test/errors_test.rb +0 -165
  96. data/test/inherit_test.rb +0 -119
  97. data/test/populate_test.rb +0 -270
  98. data/test/readonly_test.rb +0 -14
  99. data/test/save_test.rb +0 -89
  100. data/test/skip_if_test.rb +0 -74
  101. data/test/validation/dry_test.rb +0 -60
  102. data/test/validation/dry_validation_test.rb +0 -352
  103. data/test/validation/errors.yml +0 -4
@@ -1,6 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- # Specify your gem's dependencies in reform.gemspec
4
- gemspec :path => '../'
5
-
6
- gem 'minitest'
@@ -1,43 +0,0 @@
1
- class Reform::Contract::Errors
2
- def initialize(*)
3
- @errors = {}
4
- end
5
-
6
- module Merge
7
- def merge!(errors, prefix)
8
- errors.messages.each do |field, msgs|
9
- unless field.to_sym == :base
10
- field = (prefix+[field]).join(".").to_sym # TODO: why is that a symbol in Rails?
11
- end
12
-
13
- msgs.each do |msg|
14
- next if messages[field] and messages[field].include?(msg)
15
- add(field, msg)
16
- end # Forms now contains a plain errors hash. the errors for each item are still available in item.errors.
17
- end
18
- end
19
-
20
- def to_s
21
- messages.inspect
22
- end
23
- end
24
- include Merge
25
-
26
- def add(field, message)
27
- @errors[field] ||= []
28
- @errors[field] << message
29
- end
30
-
31
- def messages
32
- @errors
33
- end
34
-
35
- def empty?
36
- @errors.empty?
37
- end
38
-
39
- # needed by Rails form builder.
40
- def [](name)
41
- @errors[name] || []
42
- end
43
- end
@@ -1,37 +0,0 @@
1
- module Reform::Form::Mongoid
2
- def self.included(base)
3
- base.class_eval do
4
- register_feature Reform::Form::Mongoid
5
- include Reform::Form::ActiveModel
6
- include Reform::Form::ORM
7
- extend ClassMethods
8
- end
9
- end
10
-
11
- module ClassMethods
12
- def validates_uniqueness_of(attribute, options={})
13
- options = options.merge(:attributes => [attribute])
14
- validates_with(UniquenessValidator, options)
15
- end
16
- def i18n_scope
17
- :mongoid
18
- end
19
- end
20
-
21
-
22
- def self.mongoid_namespace
23
- if mongoid_is_4_or_more?
24
- 'Validatable'
25
- else
26
- 'Validations'
27
- end
28
- end
29
-
30
- def self.mongoid_is_4_or_more?
31
- Mongoid::VERSION.split('.').first.to_i >= 4
32
- end
33
-
34
- UniquenessValidator = Class.new("::Mongoid::#{mongoid_namespace}::UniquenessValidator".constantize) do
35
- include Reform::Form::ORM::UniquenessValidator
36
- end
37
- end
@@ -1,26 +0,0 @@
1
- module Reform::Form::ORM
2
- def model_for_property(name)
3
- return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
4
-
5
- model_name = options_for(name)[:on]
6
- model[model_name]
7
- end
8
-
9
- module UniquenessValidator
10
- # when calling validates it should create the Vali instance already and set @klass there! # TODO: fix this in AM.
11
- def validate(form)
12
- property = attributes.first
13
-
14
- # here is the thing: why does AM::UniquenessValidator require a filled-out record to work properly? also, why do we need to set
15
- # the class? it would be way easier to pass #validate a hash of attributes and get back an errors hash.
16
- # the class for the finder could either be infered from the record or set in the validator instance itself in the call to ::validates.
17
- record = form.model_for_property(property)
18
- record.send("#{property}=", form.send(property))
19
-
20
- @klass = record.class # this is usually done in the super-sucky #setup method.
21
- super(record).tap do |res|
22
- form.errors.add(property, record.errors.first.last) if record.errors.present?
23
- end
24
- end
25
- end
26
- end
@@ -1,4 +0,0 @@
1
- require 'reform/form/active_model'
2
- require 'reform/form/orm'
3
- require 'reform/form/mongoid'
4
- require 'reform/form/active_model/model_reflections' # only load this in AR context as simple_form currently is bound to AR.
@@ -1,23 +0,0 @@
1
- require "test_helper"
2
-
3
- class CallTest < Minitest::Spec
4
- Song = Struct.new(:title)
5
-
6
- class SongForm < Reform::Form
7
- property :title
8
-
9
- validation do
10
- key(:title).required
11
- end
12
- end
13
-
14
- let (:form) { SongForm.new(Song.new) }
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 }
20
-
21
- it { form.(title: "True North").errors.messages.must_equal({}) }
22
- it { form.(title: "").errors.messages.must_equal({:title=>["must be filled"]}) }
23
- end
@@ -1,149 +0,0 @@
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
@@ -1,77 +0,0 @@
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
@@ -1,27 +0,0 @@
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
@@ -1,165 +0,0 @@
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