reform 2.2.4 → 2.3.3

Sign up to get free protection for your applications and to get access to all the features.
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