reform 1.2.6 → 2.0.0.beta1
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.
- checksums.yaml +4 -4
- data/.travis.yml +6 -1
- data/CHANGES.md +14 -0
- data/Gemfile +3 -2
- data/README.md +225 -283
- data/Rakefile +27 -0
- data/TODO.md +12 -0
- data/database.sqlite3 +0 -0
- data/gemfiles/Gemfile.rails-3.0 +1 -0
- data/gemfiles/Gemfile.rails-3.1 +1 -0
- data/gemfiles/Gemfile.rails-3.2 +1 -0
- data/gemfiles/Gemfile.rails-4.0 +1 -0
- data/lib/reform.rb +0 -1
- data/lib/reform/contract.rb +64 -170
- data/lib/reform/contract/validate.rb +10 -13
- data/lib/reform/form.rb +74 -19
- data/lib/reform/form/active_model.rb +19 -14
- data/lib/reform/form/coercion.rb +1 -13
- data/lib/reform/form/composition.rb +2 -24
- data/lib/reform/form/multi_parameter_attributes.rb +43 -62
- data/lib/reform/form/populator.rb +85 -0
- data/lib/reform/form/prepopulate.rb +13 -43
- data/lib/reform/form/validate.rb +29 -90
- data/lib/reform/form/validation/unique_validator.rb +13 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +7 -7
- data/test/active_model_test.rb +43 -0
- data/test/changed_test.rb +23 -51
- data/test/coercion_test.rb +1 -7
- data/test/composition_test.rb +128 -34
- data/test/contract_test.rb +27 -86
- data/test/feature_test.rb +43 -6
- data/test/fields_test.rb +2 -12
- data/test/form_builder_test.rb +28 -25
- data/test/form_option_test.rb +19 -0
- data/test/from_test.rb +0 -75
- data/test/inherit_test.rb +178 -117
- data/test/model_reflections_test.rb +1 -1
- data/test/populate_test.rb +226 -0
- data/test/prepopulator_test.rb +112 -0
- data/test/readable_test.rb +2 -4
- data/test/save_test.rb +56 -112
- data/test/setup_test.rb +48 -0
- data/test/skip_if_test.rb +5 -2
- data/test/skip_setter_and_getter_test.rb +54 -0
- data/test/test_helper.rb +3 -1
- data/test/uniqueness_test.rb +41 -0
- data/test/validate_test.rb +325 -289
- data/test/virtual_test.rb +1 -3
- data/test/writeable_test.rb +3 -4
- metadata +35 -39
- data/lib/reform/composition.rb +0 -63
- data/lib/reform/contract/setup.rb +0 -50
- data/lib/reform/form/changed.rb +0 -9
- data/lib/reform/form/sync.rb +0 -116
- data/lib/reform/representer.rb +0 -84
- data/test/empty_test.rb +0 -58
- data/test/form_composition_test.rb +0 -145
- data/test/nested_form_test.rb +0 -197
- data/test/prepopulate_test.rb +0 -85
- data/test/sync_option_test.rb +0 -83
- data/test/sync_test.rb +0 -56
data/lib/reform/representer.rb
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
require 'representable/hash'
|
2
|
-
require 'representable/decorator'
|
3
|
-
|
4
|
-
module Reform
|
5
|
-
class Representer < Representable::Decorator
|
6
|
-
include Representable::Hash::AllowSymbols
|
7
|
-
|
8
|
-
extend Uber::InheritableAttr
|
9
|
-
inheritable_attr :options # FIXME: this doesn't need to be inheritable.
|
10
|
-
# self.options = {}
|
11
|
-
|
12
|
-
|
13
|
-
class Options < ::Hash
|
14
|
-
def include!(names)
|
15
|
-
includes.push(*names) #if names.size > 0
|
16
|
-
self
|
17
|
-
end
|
18
|
-
|
19
|
-
def exclude!(names)
|
20
|
-
excludes.push(*names) #if names.size > 0
|
21
|
-
self
|
22
|
-
end
|
23
|
-
|
24
|
-
def excludes
|
25
|
-
self[:exclude] ||= []
|
26
|
-
end
|
27
|
-
|
28
|
-
def includes
|
29
|
-
self[:include] ||= []
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
include Representable::Hash
|
35
|
-
|
36
|
-
# Returns hash of all property names.
|
37
|
-
def self.fields(&block)
|
38
|
-
representable_attrs.find_all(&block).map(&:name)
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.each(only_form=true, &block)
|
42
|
-
definitions = representable_attrs
|
43
|
-
definitions = representable_attrs.find_all { |attr| attr[:form] } if only_form
|
44
|
-
|
45
|
-
definitions.each(&block)
|
46
|
-
self
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.for(options)
|
50
|
-
clone.tap do |representer|
|
51
|
-
representer.options = options
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def self.default_inline_class
|
56
|
-
options[:form_class]
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.clone # called in inheritable_attr :representer_class.
|
60
|
-
Class.new(self) # By subclassing, representable_attrs.clone is called.
|
61
|
-
end
|
62
|
-
|
63
|
-
private
|
64
|
-
|
65
|
-
# Inline forms always get saved in :extend.
|
66
|
-
def self.build_inline(base, features, name, options, &block)
|
67
|
-
name = name.to_s.singularize.camelize
|
68
|
-
|
69
|
-
features = options[:features]
|
70
|
-
|
71
|
-
Class.new(base || default_inline_class) do
|
72
|
-
include *features
|
73
|
-
|
74
|
-
class_eval &block
|
75
|
-
|
76
|
-
@form_name = name
|
77
|
-
|
78
|
-
def self.name # needed by ActiveModel::Validation and I18N.
|
79
|
-
@form_name
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
data/test/empty_test.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
|
4
|
-
class DeprecatedVirtualTest < MiniTest::Spec # TODO: remove me in 2.0.
|
5
|
-
Location = Struct.new(:country)
|
6
|
-
|
7
|
-
class LocationForm < Reform::Form
|
8
|
-
property :country, virtual: true # this becomes readonly: true
|
9
|
-
end
|
10
|
-
|
11
|
-
let (:loc) { Location.new("Australia") }
|
12
|
-
let (:form) { LocationForm.new(loc) }
|
13
|
-
|
14
|
-
it { form.country.must_equal "Australia" }
|
15
|
-
it do
|
16
|
-
form.validate("country" => "Germany") # this usually won't change when submitting.
|
17
|
-
form.country.must_equal "Germany"
|
18
|
-
|
19
|
-
form.sync
|
20
|
-
loc.country.must_equal "Australia" # the writer wasn't called.
|
21
|
-
|
22
|
-
hash = {}
|
23
|
-
form.save do |nested|
|
24
|
-
hash = nested
|
25
|
-
end
|
26
|
-
|
27
|
-
hash.must_equal("country"=> "Germany")
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class DeprecatedEmptyTest < MiniTest::Spec # don't read, don't write
|
32
|
-
Credentials = Struct.new(:password)
|
33
|
-
|
34
|
-
class PasswordForm < Reform::Form
|
35
|
-
property :password
|
36
|
-
property :password_confirmation, empty: true
|
37
|
-
end
|
38
|
-
|
39
|
-
let (:cred) { Credentials.new }
|
40
|
-
let (:form) { PasswordForm.new(cred) }
|
41
|
-
|
42
|
-
it {
|
43
|
-
form.validate("password" => "123", "password_confirmation" => "321")
|
44
|
-
|
45
|
-
form.password.must_equal "123"
|
46
|
-
form.password_confirmation.must_equal "321" # this is still readable in the UI.
|
47
|
-
|
48
|
-
form.sync
|
49
|
-
cred.password.must_equal "123"
|
50
|
-
|
51
|
-
hash = {}
|
52
|
-
form.save do |nested|
|
53
|
-
hash = nested
|
54
|
-
end
|
55
|
-
|
56
|
-
hash.must_equal("password"=> "123", "password_confirmation" => "321")
|
57
|
-
}
|
58
|
-
end
|
@@ -1,145 +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, :empty => true, :on => :song
|
16
|
-
property :requester, :on => :requester
|
17
|
-
property :captcha, :on => :song, :empty => true
|
18
|
-
|
19
|
-
validates :name, :title, :channel, :presence => true
|
20
|
-
|
21
|
-
property :band, :on => :song do
|
22
|
-
property :title
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
let (:form) { RequestForm.new(:song => song, :requester => requester) }
|
27
|
-
let (:song) { Song.new(1, "Rio", band) }
|
28
|
-
let (:requester) { Requester.new(2, "Duran Duran", "MCP") }
|
29
|
-
let (:band) { Band.new("Duran^2") }
|
30
|
-
|
31
|
-
# delegation form -> composition works
|
32
|
-
it { form.id.must_equal 1 }
|
33
|
-
it { form.title.must_equal "Rio" }
|
34
|
-
it { form.name.must_equal "Duran Duran" }
|
35
|
-
it { form.requester_id.must_equal 2 }
|
36
|
-
it { form.channel.must_equal nil }
|
37
|
-
it { form.requester.must_equal "MCP" } # same name as composed model.
|
38
|
-
it { form.captcha.must_equal nil }
|
39
|
-
|
40
|
-
# #model just returns <Composition>.
|
41
|
-
it { form.model.must_be_kind_of Reform::Composition }
|
42
|
-
|
43
|
-
# #model[] -> composed models
|
44
|
-
it { form.model[:requester].must_equal requester }
|
45
|
-
it { form.model[:song].must_equal song }
|
46
|
-
|
47
|
-
|
48
|
-
it "creates Composition for you" do
|
49
|
-
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb").must_equal false
|
50
|
-
end
|
51
|
-
|
52
|
-
describe "#save" do
|
53
|
-
# #save with {}
|
54
|
-
it do
|
55
|
-
hash = {}
|
56
|
-
|
57
|
-
form.save do |map|
|
58
|
-
hash[:name] = form.name
|
59
|
-
hash[:title] = form.title
|
60
|
-
end
|
61
|
-
|
62
|
-
hash.must_equal({:name=>"Duran Duran", :title=>"Rio"})
|
63
|
-
end
|
64
|
-
|
65
|
-
it "provides nested symbolized hash as second block argument" do
|
66
|
-
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "channel" => "JJJ", "captcha" => "wonderful")
|
67
|
-
|
68
|
-
hash = nil
|
69
|
-
|
70
|
-
form.save do |map|
|
71
|
-
hash = map
|
72
|
-
end
|
73
|
-
|
74
|
-
hash.must_equal({
|
75
|
-
:song=>{:title=>"Greyhound", :id=>1, :channel => "JJJ", :captcha=>"wonderful", :band=>{"title"=>"Duran^2"}},
|
76
|
-
:requester=>{:name=>"Frenzal Rhomb", :id=>2, :requester => "MCP"}
|
77
|
-
}
|
78
|
-
)
|
79
|
-
end
|
80
|
-
|
81
|
-
it "xxx pushes data to models and calls #save when no block passed" do
|
82
|
-
song.extend(Saveable)
|
83
|
-
requester.extend(Saveable)
|
84
|
-
band.extend(Saveable)
|
85
|
-
|
86
|
-
form.validate("title" => "Greyhound", "name" => "Frenzal Rhomb", "captcha" => "1337")
|
87
|
-
form.captcha.must_equal "1337" # TODO: move to separate test.
|
88
|
-
|
89
|
-
form.save
|
90
|
-
|
91
|
-
requester.name.must_equal "Frenzal Rhomb"
|
92
|
-
requester.saved?.must_equal true
|
93
|
-
song.title.must_equal "Greyhound"
|
94
|
-
song.saved?.must_equal true
|
95
|
-
song.band.title.must_equal "Duran^2"
|
96
|
-
song.band.saved?.must_equal true
|
97
|
-
end
|
98
|
-
|
99
|
-
it "returns true when models all save successfully" do
|
100
|
-
song.extend(Saveable)
|
101
|
-
requester.extend(Saveable)
|
102
|
-
band.extend(Saveable)
|
103
|
-
|
104
|
-
form.save.must_equal true
|
105
|
-
end
|
106
|
-
|
107
|
-
it "returns false when one or more models don't save successfully" do
|
108
|
-
module Unsaveable
|
109
|
-
def save
|
110
|
-
false
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
song.extend(Unsaveable)
|
115
|
-
requester.extend(Saveable)
|
116
|
-
band.extend(Saveable)
|
117
|
-
|
118
|
-
form.save.must_equal false
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
|
124
|
-
class FormCompositionCollectionTest < MiniTest::Spec
|
125
|
-
Book = Struct.new(:id, :name)
|
126
|
-
Library = Struct.new(:id) do
|
127
|
-
def books
|
128
|
-
[Book.new(1,"My book")]
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
class LibraryForm < Reform::Form
|
133
|
-
include Reform::Form::Composition
|
134
|
-
|
135
|
-
collection :books, on: :library do
|
136
|
-
property :id
|
137
|
-
property :name
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
let (:form) { LibraryForm.new(library: library) }
|
142
|
-
let (:library) { Library.new(2) }
|
143
|
-
|
144
|
-
it { form.save do |hash| hash.must_equal({:library=>{:books=>[{"id"=>1, "name"=>"My book"}]}}) end }
|
145
|
-
end
|
data/test/nested_form_test.rb
DELETED
@@ -1,197 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
class NestedFormTest < MiniTest::Spec
|
4
|
-
class AlbumForm < Reform::Form
|
5
|
-
property :title
|
6
|
-
|
7
|
-
# class SongForm < Reform::Form
|
8
|
-
# property :title
|
9
|
-
# validates :title, :presence => true
|
10
|
-
# end
|
11
|
-
|
12
|
-
#form :hit, :class => SongForm
|
13
|
-
property :hit do
|
14
|
-
property :title
|
15
|
-
validates :title, :presence => true
|
16
|
-
end
|
17
|
-
|
18
|
-
collection :songs do
|
19
|
-
property :title
|
20
|
-
validates :title, :presence => true
|
21
|
-
end
|
22
|
-
|
23
|
-
validates :title, :presence => true
|
24
|
-
end
|
25
|
-
|
26
|
-
# AlbumForm::collection :songs, :form => SongForm
|
27
|
-
# should be: AlbumForm.new(songs: [Song, Song])
|
28
|
-
|
29
|
-
let (:album) do
|
30
|
-
OpenStruct.new(
|
31
|
-
:title => "Blackhawks Over Los Angeles",
|
32
|
-
:hit => song,
|
33
|
-
:songs => songs # TODO: document this requirement
|
34
|
-
)
|
35
|
-
end
|
36
|
-
let (:song) { OpenStruct.new(:title => "Downtown") }
|
37
|
-
let (:songs) { [OpenStruct.new(:title => "Calling")] }
|
38
|
-
let (:form) { AlbumForm.new(album) }
|
39
|
-
|
40
|
-
it "responds to #save" do
|
41
|
-
hsh = nil
|
42
|
-
form.save do |nested|
|
43
|
-
hsh = nested
|
44
|
-
end
|
45
|
-
hsh.must_equal({"hit"=>{"title"=>"Downtown"}, "title" => "Blackhawks Over Los Angeles", "songs"=>[{"title"=>"Calling"}]})
|
46
|
-
end
|
47
|
-
|
48
|
-
|
49
|
-
it "creates nested forms" do
|
50
|
-
form.hit.must_be_kind_of Reform::Form
|
51
|
-
form.songs.must_be_kind_of Array
|
52
|
-
end
|
53
|
-
|
54
|
-
describe "#initialize" do
|
55
|
-
describe "with empty object and no cardinality" do
|
56
|
-
let(:form) { AlbumForm.new(OpenStruct.new) }
|
57
|
-
|
58
|
-
it "allows initialization with empty properties" do
|
59
|
-
form
|
60
|
-
end
|
61
|
-
|
62
|
-
it "allows #validate" do
|
63
|
-
form.validate({})
|
64
|
-
form.errors.messages.must_equal(:title=>["can't be blank"])
|
65
|
-
end
|
66
|
-
# it "must support #validate when initialized with empty properties" do
|
67
|
-
# form.validate({})
|
68
|
-
# form.errors.messages.must_equal(:title=>["can't be blank"], :"hit.title"=>["can't be blank"], :"songs.title"=>["can't be blank"])
|
69
|
-
# end
|
70
|
-
# it "must support #validate with attributes when initialized with empty properties" do
|
71
|
-
# form.validate("hit"=>{"title"=>"Downtown"}, "title" => "Blackhawks Over Los Angeles", "songs"=>[{"title"=>"Calling"}])
|
72
|
-
# form.title.must_eql "Blackhawks Over Los Angeles"
|
73
|
-
# form.errors.messages.must_equal([])
|
74
|
-
# end
|
75
|
-
end
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
|
81
|
-
describe "rendering" do
|
82
|
-
it { form.title.must_equal "Blackhawks Over Los Angeles" }
|
83
|
-
it { form.hit.title.must_equal "Downtown" }
|
84
|
-
it { form.songs[0].title.must_equal "Calling" }
|
85
|
-
end
|
86
|
-
|
87
|
-
describe "#save" do
|
88
|
-
before { @result = form.validate(
|
89
|
-
"hit" =>{"title" => "Sacrifice"},
|
90
|
-
"title" =>"Second Heat",
|
91
|
-
"songs" => [{"title" => "Scarified"}])
|
92
|
-
}
|
93
|
-
|
94
|
-
it "updates internal Fields" do
|
95
|
-
data = {}
|
96
|
-
|
97
|
-
form.save do |nested_hash|
|
98
|
-
data[:title] = form.title
|
99
|
-
data[:hit_title] = form.hit.title
|
100
|
-
data[:first_title] = form.songs.first.title
|
101
|
-
end
|
102
|
-
|
103
|
-
data.must_equal(:title=>"Second Heat", :hit_title => "Sacrifice", :first_title => "Scarified")
|
104
|
-
end
|
105
|
-
|
106
|
-
it "returns nested hash with indifferent access" do
|
107
|
-
nested = nil
|
108
|
-
|
109
|
-
form.save do |nested_hash|
|
110
|
-
nested = nested_hash
|
111
|
-
end
|
112
|
-
|
113
|
-
nested.must_equal("title"=>"Second Heat", "hit"=>{"title"=>"Sacrifice"}, "songs"=>[{"title"=>"Scarified"}])
|
114
|
-
|
115
|
-
nested[:title].must_equal "Second Heat"
|
116
|
-
nested["title"].must_equal "Second Heat"
|
117
|
-
nested[:hit][:title].must_equal "Sacrifice"
|
118
|
-
nested["hit"]["title"].must_equal "Sacrifice"
|
119
|
-
end
|
120
|
-
|
121
|
-
it "pushes data to models" do
|
122
|
-
form.save
|
123
|
-
|
124
|
-
album.title.must_equal "Second Heat"
|
125
|
-
song.title.must_equal "Sacrifice"
|
126
|
-
songs.first.title.must_equal "Scarified"
|
127
|
-
end
|
128
|
-
|
129
|
-
describe "with invalid args" do
|
130
|
-
it "allows empty collection values" do
|
131
|
-
form.validate({})
|
132
|
-
|
133
|
-
form.songs.size.must_equal 1
|
134
|
-
form.songs[0].title.must_equal "Scarified"
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
# describe "with aliased nested form name" do
|
140
|
-
# let (:form) do
|
141
|
-
# Class.new(Reform::Form) do
|
142
|
-
# form :hit, :class => AlbumForm::SongForm, :as => :song
|
143
|
-
# end.new(OpenStruct.new(:hit => OpenStruct.new(:title => "")))
|
144
|
-
# end
|
145
|
-
|
146
|
-
# it "uses alias in errors" do
|
147
|
-
# form.validate({})
|
148
|
-
# form.errors.messages.must_equal({})
|
149
|
-
# end
|
150
|
-
# end
|
151
|
-
|
152
|
-
class ExplicitNestedFormTest < MiniTest::Spec
|
153
|
-
let (:song) { OpenStruct.new(:title => "Downtown") }
|
154
|
-
let (:album) do
|
155
|
-
OpenStruct.new(
|
156
|
-
:title => "Blackhawks Over Los Angeles",
|
157
|
-
:hit => song,
|
158
|
-
)
|
159
|
-
end
|
160
|
-
let (:form) { AlbumForm.new(album) }
|
161
|
-
|
162
|
-
class SongForm < Reform::Form
|
163
|
-
property :title
|
164
|
-
validates_presence_of :title
|
165
|
-
end
|
166
|
-
|
167
|
-
class AlbumForm < Reform::Form
|
168
|
-
property :title
|
169
|
-
|
170
|
-
property :hit, :form => SongForm #, :parse_strategy => :sync, :instance => true
|
171
|
-
end
|
172
|
-
|
173
|
-
|
174
|
-
it "allows rendering" do
|
175
|
-
form.hit.title.must_equal "Downtown"
|
176
|
-
end
|
177
|
-
|
178
|
-
it ("xxx") {
|
179
|
-
form.validate({"hit" => {"title" => ""}})
|
180
|
-
form.hit.title.must_equal ""
|
181
|
-
form.errors[:"hit.title"].must_equal(["can't be blank"])
|
182
|
-
}
|
183
|
-
end
|
184
|
-
|
185
|
-
class UnitTest < self
|
186
|
-
it "keeps Forms for form collection" do
|
187
|
-
form.send(:fields).songs.must_be_kind_of Array
|
188
|
-
end
|
189
|
-
|
190
|
-
describe "#validate" do
|
191
|
-
it "keeps Form instances" do
|
192
|
-
form.validate("songs"=>[{"title" => "Atwa"}])
|
193
|
-
form.songs.first.must_be_kind_of Reform::Form
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|