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