formality 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +253 -0
- data/Rakefile +38 -0
- data/test/attributes.rb +89 -0
- data/test/compliance.rb +13 -0
- data/test/lint.rb +77 -0
- data/test/nest_many.rb +116 -0
- data/test/nest_one.rb +106 -0
- data/test/rails.rb +88 -0
- data/test/valid.rb +91 -0
- metadata +116 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Arun Srinivasan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
# Formality
|
2
|
+
|
3
|
+
Form objects. For rails.
|
4
|
+
|
5
|
+
gem install formality
|
6
|
+
|
7
|
+
Forms have both data and behavior. Sounds suspiciously like they should be
|
8
|
+
objects. Let's make them so.
|
9
|
+
|
10
|
+
Additionally, let's get all that validation logic out of the ORM. Enforcing
|
11
|
+
data integrity is one thing; making sure that you get either a phone number or
|
12
|
+
an email from a user is another.
|
13
|
+
|
14
|
+
## Quick Example
|
15
|
+
|
16
|
+
Let's make a signup form.
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
class SignupForm
|
20
|
+
include Formality
|
21
|
+
|
22
|
+
attribute :email
|
23
|
+
attribute :password
|
24
|
+
attribute :password_confirmation
|
25
|
+
|
26
|
+
validates_presence_of :email, :password, :password_confirmation
|
27
|
+
validates_confirmation_of :password
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
In, say, a UsersController:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class UsersController < ApplicaationController
|
35
|
+
def new
|
36
|
+
@form = SignupForm.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def create
|
40
|
+
@form = SignupForm.assign params[:signup_form]
|
41
|
+
|
42
|
+
@form.valid do
|
43
|
+
User.create! @form.attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
@form.invalid do
|
47
|
+
render :new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
In the view:
|
54
|
+
|
55
|
+
```haml
|
56
|
+
= form_for @form do |f|
|
57
|
+
= f.text_field :email, :placeholder => "Email"
|
58
|
+
= f.password_field :password, :placeholder => "Password"
|
59
|
+
= f.password_field :password_confirmation, :placeholder => "Confirm Password"
|
60
|
+
= f.submit "Sign up!"
|
61
|
+
```
|
62
|
+
|
63
|
+
## More docs
|
64
|
+
|
65
|
+
#### Attributes
|
66
|
+
|
67
|
+
Use the `attribute` class method to declare attributes. It accepts a `:default`
|
68
|
+
option.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class TodoForm
|
72
|
+
include Formality
|
73
|
+
|
74
|
+
attribute :description
|
75
|
+
attribute :done, :default => false
|
76
|
+
end
|
77
|
+
|
78
|
+
default = TodoForm.new
|
79
|
+
default.description #=> nil
|
80
|
+
default.done #=> false
|
81
|
+
```
|
82
|
+
|
83
|
+
`assign` is available as a class or instance method, and will:
|
84
|
+
|
85
|
+
1. Assign any declared attributes from the hash it receives.
|
86
|
+
2. Assign an `:id` if one is present, whether declared or not (this allows us
|
87
|
+
to get some nice behavior when working with models).
|
88
|
+
3. Return the form object.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# Continuing using the TodoForm
|
92
|
+
|
93
|
+
form = TodoForm.assign :id => 1, :description => "Buy some milk"
|
94
|
+
# or
|
95
|
+
form = TodoForm.new.assign :id => 1, :description => "Buy some milk"
|
96
|
+
|
97
|
+
form.id #=> 1
|
98
|
+
form.description #=> "Buy some milk"
|
99
|
+
form.done #=> false
|
100
|
+
```
|
101
|
+
|
102
|
+
`attributes` gets you a Hash of the attributes:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
form = TodoForm.assign :id => 1, :description => "Buy some milk"
|
106
|
+
|
107
|
+
form.attributes #=> {"description" => "Buy some milk", "done" => false}
|
108
|
+
|
109
|
+
# It's an ActiveSupport::HashWithIndifferentAccess, so:
|
110
|
+
attrs = form.attributes
|
111
|
+
attrs[:description] == attrs["description"] == "Buy some milk"
|
112
|
+
```
|
113
|
+
|
114
|
+
`attribute_names` returns an Array of attribute names:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
form = TodoForm.new
|
118
|
+
form.attribute_names #=> ["description", "done"]
|
119
|
+
```
|
120
|
+
|
121
|
+
#### Validations
|
122
|
+
|
123
|
+
This one's easy to write docs for, as they are [already
|
124
|
+
written][validddocs].
|
125
|
+
|
126
|
+
You get all your standard ActiveModel validations to play with:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class SignupForm
|
130
|
+
include Formality
|
131
|
+
|
132
|
+
attribute :email
|
133
|
+
attribute :password
|
134
|
+
attribute :password_confirmation
|
135
|
+
|
136
|
+
validates_presence_of :email, :password, :password_confirmation,
|
137
|
+
:message => "can't be blank"
|
138
|
+
validates_confirmation_of :password,
|
139
|
+
:message => "confirmation mismatch"
|
140
|
+
end
|
141
|
+
|
142
|
+
form = SignupForm.assign :password => "123", :password_confirmation => "456"
|
143
|
+
form.valid? #=> false
|
144
|
+
form.invalid? #=> true
|
145
|
+
form.errors[:email] #=> ["can't be blank"]
|
146
|
+
form.errors[:password] #=> ["confirmation mismatch"]
|
147
|
+
```
|
148
|
+
|
149
|
+
You can, of course, use `:valid?` and `:invalid?` directly, but Formality also
|
150
|
+
provides with a little bit of sugar:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
form = SignupForm.assign(some_attrs_from_somewhere)
|
154
|
+
|
155
|
+
form.valid do
|
156
|
+
# this block will run only if the form is valid
|
157
|
+
end
|
158
|
+
|
159
|
+
form.invalid do
|
160
|
+
# this block will run only if the form is NOT valid
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
It's a couple of lines longer than `if form.valid?...else...end`, but I feel it
|
165
|
+
reads a little more clearly. Use it if you like it, don't if you don't.
|
166
|
+
|
167
|
+
#### Working With Models
|
168
|
+
|
169
|
+
The `from_model` class method builds a from object from an associated model.
|
170
|
+
The model object must have an `attributes` method (in case you're looking at
|
171
|
+
using this with something that's not ActiveRecord).
|
172
|
+
|
173
|
+
The `model` class method lets you specify which model the form object
|
174
|
+
represents. This lets `form_for` generate the right resourceful routes for your
|
175
|
+
object from the form object itself.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# config.routes.rb
|
179
|
+
resources :todos
|
180
|
+
|
181
|
+
# models/todo.rb
|
182
|
+
class Todo < ActiveRecord::Base; end
|
183
|
+
|
184
|
+
# models/forms/todo_form.rb (or wherever you want to put it)
|
185
|
+
class TodoForm
|
186
|
+
include Formality
|
187
|
+
|
188
|
+
model :todo
|
189
|
+
|
190
|
+
attribute :description
|
191
|
+
attribute :done, :default => false
|
192
|
+
end
|
193
|
+
|
194
|
+
todo = Todo.create!(:description => "Feed the dog")
|
195
|
+
todo.id #=> 42 (or whatever)
|
196
|
+
|
197
|
+
form = TodoForm.from_model(todo)
|
198
|
+
form.id #=> 42
|
199
|
+
form.description #=> "Feed the dog"
|
200
|
+
form.done #=> false
|
201
|
+
|
202
|
+
# Now, when you pass your form into `form_for`, it will
|
203
|
+
# automatically create a form that points to "/todos/42"
|
204
|
+
# with the method set to put.
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Nesting
|
208
|
+
|
209
|
+
Use `nest_many` or `nest_one` to declare nested form objects. This will define
|
210
|
+
reader and writer methods such that the form object will work cleanly with
|
211
|
+
a `fields_for` call.
|
212
|
+
|
213
|
+
If you also add a `:from_model_attribute` option, nested form objects will get
|
214
|
+
correctly populated when you use `from_model`.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
Question = Struct.new(:id, :text, :answers) do
|
218
|
+
def attributes; Hash[members.zip(values)] end
|
219
|
+
end
|
220
|
+
|
221
|
+
Answer = Struct.new(:id, :text) do
|
222
|
+
def attributes; Hash[members.zip(values)] end
|
223
|
+
end
|
224
|
+
|
225
|
+
class QuestionForm
|
226
|
+
include Formality
|
227
|
+
attribute :text
|
228
|
+
nest_many :answer_forms, :from_model_attribute => :answers
|
229
|
+
validates_presence_of :text, :message => "can't be empty"
|
230
|
+
end
|
231
|
+
|
232
|
+
class AnswerForm
|
233
|
+
include Formality
|
234
|
+
attribute :text
|
235
|
+
validates_presence_of :text, :message => "can't be empty"
|
236
|
+
end
|
237
|
+
|
238
|
+
question = Question.new(1, "Question 1")
|
239
|
+
question.answers = [Answer.new(1, "Answer 1"), Answer.new(2, "Answer 2")]
|
240
|
+
|
241
|
+
form = QuestionForm.from_model(question)
|
242
|
+
form.attributes #=> {"text" => "Question 1",
|
243
|
+
"answer_forms_attributes" => [{"text"=>"Answer 1"},
|
244
|
+
{"text"=>"Answer 2"}]}
|
245
|
+
|
246
|
+
form.answer_forms[0].id #=> 1
|
247
|
+
form.answer_forms[1].id #=> 2
|
248
|
+
```
|
249
|
+
|
250
|
+
I think there are still maybe some API kinks to work out with the nested forms,
|
251
|
+
but this gets it most of the way there.
|
252
|
+
|
253
|
+
[validdocs]: http://guides.rubyonrails.org/active_record_validations_callbacks.html
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
$:.push("lib")
|
2
|
+
require "formality"
|
3
|
+
|
4
|
+
desc "Run the tests"
|
5
|
+
task :test do
|
6
|
+
require "tst"
|
7
|
+
Tst.run "test/*.rb",
|
8
|
+
:load_paths => ['.', 'lib'],
|
9
|
+
:reporter => Tst::Reporters::Pretty.new
|
10
|
+
end
|
11
|
+
|
12
|
+
NAME = "formality"
|
13
|
+
VERSION = Formality::VERSION
|
14
|
+
|
15
|
+
namespace :gem do
|
16
|
+
desc 'Clean up generated files'
|
17
|
+
task :clean do
|
18
|
+
sh 'rm -rf pkg'
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Build the gem"
|
22
|
+
task :build => :clean do
|
23
|
+
sh "mkdir pkg"
|
24
|
+
sh "gem build #{NAME}.gemspec"
|
25
|
+
sh "mv #{NAME}-#{VERSION}.gem pkg/"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Release v#{VERSION}"
|
29
|
+
task :release => :build do
|
30
|
+
sh "git commit --allow-empty -a -m 'Release #{VERSION}'"
|
31
|
+
sh "git tag v#{VERSION}"
|
32
|
+
sh "git push origin master"
|
33
|
+
sh "git push origin v#{VERSION}"
|
34
|
+
sh "gem push pkg/#{NAME}-#{VERSION}.gem"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :test
|
data/test/attributes.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'test/lint'
|
2
|
+
require 'formality'
|
3
|
+
|
4
|
+
class ::Form
|
5
|
+
include Formality
|
6
|
+
attribute :no_default
|
7
|
+
attribute :answer, :default => 42
|
8
|
+
end
|
9
|
+
|
10
|
+
lint(Form.new)
|
11
|
+
|
12
|
+
tst ".attribute defines reader and writer" do
|
13
|
+
assert Form.new.respond_to? :answer
|
14
|
+
assert Form.new.respond_to? :answer=
|
15
|
+
end
|
16
|
+
|
17
|
+
tst "an attribute without a :default defaults to nil" do
|
18
|
+
assert_equal nil, Form.new.no_default
|
19
|
+
end
|
20
|
+
|
21
|
+
tst ":default option sets a default value" do
|
22
|
+
assert_equal 42, Form.new.answer
|
23
|
+
end
|
24
|
+
|
25
|
+
tst "#attribute_names returns an Array of attribute names" do
|
26
|
+
assert_equal ["no_default", "answer"], Form.new.attribute_names
|
27
|
+
end
|
28
|
+
|
29
|
+
tst "#attributes returns a HashWithIndifferentAccess" do
|
30
|
+
form = Form.new
|
31
|
+
assert ActiveSupport::HashWithIndifferentAccess === form.attributes
|
32
|
+
end
|
33
|
+
|
34
|
+
tst "#attributes hash contains all the attribute values" do
|
35
|
+
form = Form.new
|
36
|
+
assert_equal nil, form.attributes[:no_default]
|
37
|
+
assert_equal 42, form.attributes[:answer]
|
38
|
+
end
|
39
|
+
|
40
|
+
tst "#attribute? returns true if the attribute was declared" do
|
41
|
+
form = Form.new
|
42
|
+
assert form.attribute?(:answer)
|
43
|
+
assert form.attribute?(:no_default)
|
44
|
+
assert form.attribute?("answer")
|
45
|
+
assert form.attribute?("no_default")
|
46
|
+
end
|
47
|
+
|
48
|
+
tst "#assign assigns attribute values" do
|
49
|
+
form = Form.new
|
50
|
+
form.assign :answer => "hello", :no_default => "some value"
|
51
|
+
|
52
|
+
assert_equal "some value", form.attributes[:no_default]
|
53
|
+
assert_equal "hello", form.attributes[:answer]
|
54
|
+
end
|
55
|
+
|
56
|
+
tst "#assign returns the form instance for chaining/assignment convenience" do
|
57
|
+
form = Form.new
|
58
|
+
the_same = form.assign :answer => "hello", :no_default => "some value"
|
59
|
+
|
60
|
+
assert_equal form, the_same
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
tst "#assign does not assign undeclared attributes" do
|
65
|
+
form = Form.new
|
66
|
+
def form.undeclared=(*args) raise("shouldn't get here") end
|
67
|
+
form.assign(:undeclared => 123,
|
68
|
+
:answer => "hello",
|
69
|
+
:no_default => "some value")
|
70
|
+
|
71
|
+
assert_equal "some value", form.attributes[:no_default]
|
72
|
+
assert_equal "hello", form.attributes[:answer]
|
73
|
+
end
|
74
|
+
|
75
|
+
tst "#assign will assign to @id if an :id key comes in" do
|
76
|
+
form = Form.new
|
77
|
+
form.assign :id => "123", :answer => "hello", :no_default => "some value"
|
78
|
+
assert_equal "123", form.id
|
79
|
+
end
|
80
|
+
|
81
|
+
tst ".assign: just a thin wrapper to not have to call new" do
|
82
|
+
form = Form.assign :answer => "hello", :no_default => "some value"
|
83
|
+
|
84
|
+
assert form.kind_of?(Form)
|
85
|
+
assert_equal "hello", form.answer
|
86
|
+
assert_equal "some value", form.no_default
|
87
|
+
end
|
88
|
+
|
89
|
+
Object.send(:remove_const, :Form)
|
data/test/compliance.rb
ADDED
data/test/lint.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#
|
2
|
+
# Checks `model` for ActiveModel compliance.
|
3
|
+
#
|
4
|
+
# The tests are ported directly from ActiveModel::Lint
|
5
|
+
#
|
6
|
+
|
7
|
+
def lint(model)
|
8
|
+
tst "responds to :to_key" do
|
9
|
+
assert model.respond_to? :to_key
|
10
|
+
end
|
11
|
+
|
12
|
+
tst ":to_key returns nil when :persisted? returns false" do
|
13
|
+
form = model
|
14
|
+
def form.persisted?; false end
|
15
|
+
assert_equal nil, form.to_key
|
16
|
+
end
|
17
|
+
|
18
|
+
tst "responds to :to_param" do
|
19
|
+
assert model.respond_to? :to_param
|
20
|
+
end
|
21
|
+
|
22
|
+
tst ":to_param returns nil when :persisted? returns false" do
|
23
|
+
form = model
|
24
|
+
def form.persisted?; false end
|
25
|
+
assert_equal nil, form.to_param
|
26
|
+
end
|
27
|
+
|
28
|
+
tst "responds to :to_partial_path" do
|
29
|
+
assert model.respond_to? :to_partial_path
|
30
|
+
end
|
31
|
+
|
32
|
+
tst ":to_partial_path returns a String" do
|
33
|
+
assert String === model.to_partial_path
|
34
|
+
end
|
35
|
+
|
36
|
+
tst "responds to :valid?" do
|
37
|
+
assert model.respond_to? :valid?
|
38
|
+
end
|
39
|
+
|
40
|
+
tst ":valid? returns a Boolean" do
|
41
|
+
valid = model.valid?
|
42
|
+
assert valid === true || valid === false
|
43
|
+
end
|
44
|
+
|
45
|
+
tst "responds to :persisted?" do
|
46
|
+
assert model.respond_to? :persisted?
|
47
|
+
end
|
48
|
+
|
49
|
+
tst ":persisted? returns a Boolean" do
|
50
|
+
persisted = model.persisted?
|
51
|
+
assert persisted === true || persisted === false
|
52
|
+
end
|
53
|
+
|
54
|
+
tst "responds to :model_name" do
|
55
|
+
assert model.class.respond_to? :model_name
|
56
|
+
end
|
57
|
+
|
58
|
+
tst ":model_name returns a string with convenience methods" do
|
59
|
+
name = model.class.model_name
|
60
|
+
assert String === name
|
61
|
+
assert String === name.human
|
62
|
+
assert String === name.singular
|
63
|
+
assert String === name.plural
|
64
|
+
end
|
65
|
+
|
66
|
+
tst "responds to :errors" do
|
67
|
+
assert model.respond_to? :errors
|
68
|
+
end
|
69
|
+
|
70
|
+
tst "error#[] returns an Array" do
|
71
|
+
assert Array === model.errors[:hello]
|
72
|
+
end
|
73
|
+
|
74
|
+
tst "errors#full_messages returns an Array" do
|
75
|
+
assert Array === model.errors.full_messages
|
76
|
+
end
|
77
|
+
end
|
data/test/nest_many.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'test/lint'
|
2
|
+
require 'formality'
|
3
|
+
|
4
|
+
::Question = Struct.new(:id, :text, :answers) do
|
5
|
+
def attributes; Hash[members.zip(values)] end
|
6
|
+
end
|
7
|
+
|
8
|
+
::Answer = Struct.new(:id, :text) do
|
9
|
+
def attributes; Hash[members.zip(values)] end
|
10
|
+
end
|
11
|
+
|
12
|
+
class ::QuestionForm
|
13
|
+
include Formality
|
14
|
+
attribute :text
|
15
|
+
nest_many :answer_forms, :from_model_attribute => :answers
|
16
|
+
validates_presence_of :text, :message => "can't be empty"
|
17
|
+
end
|
18
|
+
|
19
|
+
class ::AnswerForm
|
20
|
+
include Formality
|
21
|
+
attribute :text
|
22
|
+
validates_presence_of :text, :message => "can't be empty"
|
23
|
+
end
|
24
|
+
|
25
|
+
lint(AnswerForm.new)
|
26
|
+
lint(QuestionForm.new)
|
27
|
+
|
28
|
+
tst ".nest_many: defines an '<assoc>' reader" do
|
29
|
+
form = QuestionForm.new
|
30
|
+
assert form.respond_to?(:answer_forms)
|
31
|
+
end
|
32
|
+
|
33
|
+
tst ".nest_many: defines an '<assoc>_attributes' reader" do
|
34
|
+
form = QuestionForm.new
|
35
|
+
assert form.respond_to?(:answer_forms_attributes)
|
36
|
+
end
|
37
|
+
|
38
|
+
tst ".nest_many: reader defaults to an empty array" do
|
39
|
+
form = QuestionForm.new
|
40
|
+
assert_equal [], form.answer_forms
|
41
|
+
end
|
42
|
+
|
43
|
+
tst ".nest_many: defines an '<assoc>_attributes=' writer" do
|
44
|
+
form = QuestionForm.new
|
45
|
+
assert form.respond_to?(:answer_forms_attributes=)
|
46
|
+
end
|
47
|
+
|
48
|
+
tst ".nest_many: reader returns an Array of child form objects" do
|
49
|
+
form = QuestionForm.new
|
50
|
+
form.answer_forms_attributes = [{:text => "first"}]
|
51
|
+
answer_form = form.answer_forms.first
|
52
|
+
|
53
|
+
assert answer_form.kind_of?(AnswerForm)
|
54
|
+
assert_equal "first", answer_form.text
|
55
|
+
end
|
56
|
+
|
57
|
+
tst "#attributes: includes the nested form's attributes" do
|
58
|
+
form = QuestionForm.new
|
59
|
+
form.assign :answer_forms_attributes => [{:text => "first"}]
|
60
|
+
|
61
|
+
|
62
|
+
expected = {"text" => nil,
|
63
|
+
"answer_forms_attributes" => [{"text" => "first"}]}
|
64
|
+
|
65
|
+
assert_equal expected, form.attributes
|
66
|
+
end
|
67
|
+
|
68
|
+
tst "#valid?: returns false if any nested forms are invalid" do
|
69
|
+
form = QuestionForm.new.assign :text => "question text"
|
70
|
+
form.answer_forms_attributes = [{:text => ""}]
|
71
|
+
|
72
|
+
assert_equal false, form.valid?
|
73
|
+
end
|
74
|
+
|
75
|
+
tst "valid?: sets errors on the nested forms" do
|
76
|
+
form = QuestionForm.new.assign :text => "question text"
|
77
|
+
form.answer_forms_attributes = [{:text => ""}]
|
78
|
+
form.valid?
|
79
|
+
answer_form = form.answer_forms.first
|
80
|
+
|
81
|
+
assert_equal ["can't be empty"], answer_form.errors[:text]
|
82
|
+
end
|
83
|
+
|
84
|
+
tst "#invalid?: returns true if any nested forms are invalid" do
|
85
|
+
form = QuestionForm.new.assign :text => "question text"
|
86
|
+
form.answer_forms_attributes = [{:text => ""}]
|
87
|
+
|
88
|
+
assert_equal true, form.invalid?
|
89
|
+
end
|
90
|
+
|
91
|
+
tst "#invalid?: sets errors on the nested forms" do
|
92
|
+
form = QuestionForm.new.assign :text => "question text"
|
93
|
+
form.answer_forms_attributes = [{:text => ""}]
|
94
|
+
form.invalid?
|
95
|
+
answer_form = form.answer_forms.first
|
96
|
+
|
97
|
+
assert_equal ["can't be empty"], answer_form.errors[:text]
|
98
|
+
end
|
99
|
+
|
100
|
+
tst "#from_model: assigns to nested form from nested models" do
|
101
|
+
question = Question.new(1, "Question 1")
|
102
|
+
question.answers = [Answer.new(1, "Answer 1"), Answer.new(2, "Answer 2")]
|
103
|
+
form = QuestionForm.from_model(question)
|
104
|
+
|
105
|
+
answer_form_1, answer_form_2 = form.answer_forms
|
106
|
+
|
107
|
+
assert_equal 1, answer_form_1.id
|
108
|
+
assert_equal "Answer 1", answer_form_1.text
|
109
|
+
assert_equal 2, answer_form_2.id
|
110
|
+
assert_equal "Answer 2", answer_form_2.text
|
111
|
+
end
|
112
|
+
|
113
|
+
Object.send(:remove_const, :QuestionForm)
|
114
|
+
Object.send(:remove_const, :AnswerForm)
|
115
|
+
Object.send(:remove_const, :Question)
|
116
|
+
Object.send(:remove_const, :Answer)
|
data/test/nest_one.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'test/lint'
|
2
|
+
require 'formality'
|
3
|
+
|
4
|
+
::Question = Struct.new(:id, :text, :answer) do
|
5
|
+
def attributes; Hash[members.zip(values)] end
|
6
|
+
end
|
7
|
+
|
8
|
+
::Answer = Struct.new(:id, :text) do
|
9
|
+
def attributes; Hash[members.zip(values)] end
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
class ::QuestionForm
|
14
|
+
include Formality
|
15
|
+
attribute :text
|
16
|
+
nest_one :answer_form, :from_model_attribute => :answer
|
17
|
+
validates_presence_of :text, :message => "can't be empty"
|
18
|
+
end
|
19
|
+
|
20
|
+
class ::AnswerForm
|
21
|
+
include Formality
|
22
|
+
attribute :text
|
23
|
+
validates_presence_of :text, :message => "can't be empty"
|
24
|
+
end
|
25
|
+
|
26
|
+
tst ".nest_one: defines an '<assoc>' reader" do
|
27
|
+
form = QuestionForm.new
|
28
|
+
assert form.respond_to?(:answer_form)
|
29
|
+
end
|
30
|
+
|
31
|
+
tst ".nest_one: defines an '<assoc>_attributes' reader" do
|
32
|
+
form = QuestionForm.new
|
33
|
+
assert form.respond_to?(:answer_form_attributes)
|
34
|
+
end
|
35
|
+
|
36
|
+
tst ".nest_one: reader defaults to nil" do
|
37
|
+
form = QuestionForm.new
|
38
|
+
assert_equal nil, form.answer_form
|
39
|
+
end
|
40
|
+
|
41
|
+
tst ".nest_one: defines an '<assoc>_attributes=' writer" do
|
42
|
+
form = QuestionForm.new
|
43
|
+
assert form.respond_to?(:answer_form_attributes=)
|
44
|
+
end
|
45
|
+
|
46
|
+
tst ".nest_many: reader returns a child form object" do
|
47
|
+
form = QuestionForm.new
|
48
|
+
form.answer_form_attributes = {:text => "first"}
|
49
|
+
|
50
|
+
assert form.answer_form.kind_of?(AnswerForm)
|
51
|
+
assert_equal "first", form.answer_form.text
|
52
|
+
end
|
53
|
+
|
54
|
+
tst "#attributes: includes the nested form's attributes" do
|
55
|
+
form = QuestionForm.new
|
56
|
+
form.answer_form_attributes = {:text => "first"}
|
57
|
+
|
58
|
+
expected = {"text" => nil,
|
59
|
+
"answer_form_attributes" => {"text" => "first"}}
|
60
|
+
|
61
|
+
assert_equal expected, form.attributes
|
62
|
+
end
|
63
|
+
|
64
|
+
tst "#valid?: returns false if the nested form is invalid" do
|
65
|
+
form = QuestionForm.new.assign :text => "question text"
|
66
|
+
form.answer_form_attributes = {:text => ""}
|
67
|
+
|
68
|
+
assert_equal false, form.valid?
|
69
|
+
end
|
70
|
+
|
71
|
+
tst "valid?: sets errors on the nested form" do
|
72
|
+
form = QuestionForm.new.assign :text => "question text"
|
73
|
+
form.answer_form_attributes = {:text => ""}
|
74
|
+
form.valid?
|
75
|
+
|
76
|
+
assert_equal ["can't be empty"], form.answer_form.errors[:text]
|
77
|
+
end
|
78
|
+
|
79
|
+
tst "#invalid?: returns true if the nested form is invalid" do
|
80
|
+
form = QuestionForm.new.assign :text => "question text"
|
81
|
+
form.answer_form_attributes = {:text => ""}
|
82
|
+
|
83
|
+
assert_equal true, form.invalid?
|
84
|
+
end
|
85
|
+
|
86
|
+
tst "invalid?: sets errors on the nested form" do
|
87
|
+
form = QuestionForm.new.assign :text => "question text"
|
88
|
+
form.answer_form_attributes = {:text => ""}
|
89
|
+
form.invalid?
|
90
|
+
|
91
|
+
assert_equal ["can't be empty"], form.answer_form.errors[:text]
|
92
|
+
end
|
93
|
+
|
94
|
+
tst "#from_model: assigns to nested form from nested model" do
|
95
|
+
question = Question.new(1, "Question 1")
|
96
|
+
question.answer = Answer.new(1, "Answer 1")
|
97
|
+
form = QuestionForm.from_model(question)
|
98
|
+
|
99
|
+
assert_equal 1, form.answer_form.id
|
100
|
+
assert_equal "Answer 1", form.answer_form.text
|
101
|
+
end
|
102
|
+
|
103
|
+
Object.send(:remove_const, :QuestionForm)
|
104
|
+
Object.send(:remove_const, :AnswerForm)
|
105
|
+
Object.send(:remove_const, :Question)
|
106
|
+
Object.send(:remove_const, :Answer)
|
data/test/rails.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'test/lint'
|
2
|
+
require 'formality'
|
3
|
+
require 'action_dispatch/routing'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Some tests to make sure that Formality forms work
|
7
|
+
# well with the Rails view helpers (:form_for, et al.)
|
8
|
+
#
|
9
|
+
|
10
|
+
# A little fake AR-like model
|
11
|
+
::Person = Struct.new(:id, :first, :last) do
|
12
|
+
def attributes; Hash[members.zip(values)] end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Our intrepid hero
|
16
|
+
class ::PersonForm
|
17
|
+
include Formality
|
18
|
+
attribute :first
|
19
|
+
attribute :last
|
20
|
+
end
|
21
|
+
|
22
|
+
lint(PersonForm.new)
|
23
|
+
|
24
|
+
# For testing correct route generation delegation
|
25
|
+
class Route
|
26
|
+
include ActionDispatch::Routing::UrlFor
|
27
|
+
|
28
|
+
def self.url_for(*args)
|
29
|
+
new.url_for(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def person_url(person) "person/#{person.id}" end
|
33
|
+
def people_url; "people" end
|
34
|
+
end
|
35
|
+
|
36
|
+
tst "#from_model assigns form's attributes" do
|
37
|
+
person = Person.new(1, "Arthur", "Dent")
|
38
|
+
form = PersonForm.from_model(person)
|
39
|
+
|
40
|
+
assert_equal "Arthur", form.first
|
41
|
+
assert_equal "Dent", form.last
|
42
|
+
end
|
43
|
+
|
44
|
+
tst "#from_model assigns form's id" do
|
45
|
+
person = Person.new(1, "Arthur", "Dent")
|
46
|
+
form = PersonForm.from_model(person)
|
47
|
+
|
48
|
+
assert_equal 1, form.id
|
49
|
+
end
|
50
|
+
|
51
|
+
tst "just to confirm, :id doesn't show up in #attributes" do
|
52
|
+
person = Person.new(1, "Arthur", "Dent")
|
53
|
+
form = PersonForm.from_model(person)
|
54
|
+
|
55
|
+
assert !form.attributes.has_key?(:id)
|
56
|
+
assert !form.attributes.has_key?("id")
|
57
|
+
end
|
58
|
+
|
59
|
+
tst "#persisted? is true if there's an id present" do
|
60
|
+
person = Person.new(1, "Arthur", "Dent")
|
61
|
+
form = PersonForm.from_model(person)
|
62
|
+
|
63
|
+
assert form.persisted?
|
64
|
+
end
|
65
|
+
|
66
|
+
tst ".model method lets you set the model for the form" do
|
67
|
+
klass = PersonForm.dup
|
68
|
+
klass.model :person
|
69
|
+
|
70
|
+
assert_equal "Person", klass.model_name
|
71
|
+
assert_equal "people", klass.model_name.route_key
|
72
|
+
assert_equal "person", klass.model_name.singular_route_key
|
73
|
+
end
|
74
|
+
|
75
|
+
tst ".model lets rails generate routes for underlying model" do
|
76
|
+
klass = PersonForm.dup
|
77
|
+
klass.model :person
|
78
|
+
|
79
|
+
dent = Person.new(1, "Arthur", "Dent")
|
80
|
+
dent_form = klass.from_model(dent)
|
81
|
+
anon_form = klass.new
|
82
|
+
|
83
|
+
assert_equal "person/1", Route.url_for(dent_form)
|
84
|
+
assert_equal "people", Route.url_for(anon_form)
|
85
|
+
end
|
86
|
+
|
87
|
+
Object.send(:remove_const, :Person)
|
88
|
+
Object.send(:remove_const, :PersonForm)
|
data/test/valid.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'test/lint'
|
2
|
+
require 'formality'
|
3
|
+
|
4
|
+
# A form class with validations
|
5
|
+
class ::LoginForm
|
6
|
+
include Formality
|
7
|
+
|
8
|
+
attribute :email
|
9
|
+
attribute :password
|
10
|
+
attribute :password_confirmation
|
11
|
+
|
12
|
+
validates_presence_of :email, :password, :password_confirmation,
|
13
|
+
:message => "can't be empty"
|
14
|
+
|
15
|
+
validates_confirmation_of :password,
|
16
|
+
:message => "don't match"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Check that we still pass ActiveModel::Lint
|
20
|
+
lint(LoginForm.new)
|
21
|
+
|
22
|
+
# A little helper to generate valid attributes
|
23
|
+
def valid_attributes
|
24
|
+
{ :email => 'test@example.com',
|
25
|
+
:password => '123',
|
26
|
+
:password_confirmation => '123' }
|
27
|
+
end
|
28
|
+
|
29
|
+
tst "the builtin ActiveModel validations exist" do
|
30
|
+
[ :validates, :validates!, :validate, :validates_each,
|
31
|
+
:validates_confirmation_of, :validates_exclusion_of,
|
32
|
+
:validates_inclusion_of, :validates_format_of, :validates_length_of,
|
33
|
+
:validates_numericality_of, :validates_presence_of, :validates_with
|
34
|
+
].each do |validator|
|
35
|
+
assert LoginForm.respond_to?(validator)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
tst "#valid? and #invalid? do the right thing when conditions ARE NOT met" do
|
40
|
+
form = LoginForm.new
|
41
|
+
assert_equal false, form.valid?
|
42
|
+
assert_equal true, form.invalid?
|
43
|
+
end
|
44
|
+
|
45
|
+
tst "#valid? and #invalid? do the right thing when conditions ARE met" do
|
46
|
+
form = LoginForm.new.assign(valid_attributes)
|
47
|
+
assert_equal true, form.valid?
|
48
|
+
assert_equal false, form.invalid?
|
49
|
+
end
|
50
|
+
|
51
|
+
tst "#errors contains the error messages" do
|
52
|
+
form = LoginForm.new
|
53
|
+
|
54
|
+
assert_equal false, form.valid?
|
55
|
+
assert_equal ["can't be empty"], form.errors[:email]
|
56
|
+
assert_equal ["can't be empty"], form.errors[:password]
|
57
|
+
assert_equal ["can't be empty"], form.errors[:password_confirmation]
|
58
|
+
|
59
|
+
form.email = "test@example.com"
|
60
|
+
form.password = "123"
|
61
|
+
form.password_confirmation = "1234"
|
62
|
+
|
63
|
+
assert_equal false, form.valid?
|
64
|
+
assert_equal ["don't match"], form.errors[:password]
|
65
|
+
end
|
66
|
+
|
67
|
+
tst "#valid runs its block if the form is valid" do
|
68
|
+
form = LoginForm.new.assign(valid_attributes)
|
69
|
+
called = 0
|
70
|
+
form.valid { called += 1 }
|
71
|
+
assert_equal 1, called
|
72
|
+
end
|
73
|
+
|
74
|
+
tst "#valid does NOT run its block if the form is invalid" do
|
75
|
+
form = LoginForm.new
|
76
|
+
form.valid { raise("it's an error if you get here") }
|
77
|
+
end
|
78
|
+
|
79
|
+
tst "#invalid runs its block if the form is invalid" do
|
80
|
+
form = LoginForm.new
|
81
|
+
called = 0
|
82
|
+
form.invalid { called += 1 }
|
83
|
+
assert_equal 1, called
|
84
|
+
end
|
85
|
+
|
86
|
+
tst "#invalid does NOT run its block if the form is valid" do
|
87
|
+
form = LoginForm.new.assign(valid_attributes)
|
88
|
+
form.invalid { raise("it's an error if you get here") }
|
89
|
+
end
|
90
|
+
|
91
|
+
Object.send(:remove_const, :LoginForm)
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: formality
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Arun Srinivasan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activemodel
|
16
|
+
requirement: &70175687157900 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70175687157900
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activesupport
|
27
|
+
requirement: &70175687157400 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.0.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70175687157400
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: actionpack
|
38
|
+
requirement: &70175687156940 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 3.0.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70175687156940
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: tst
|
49
|
+
requirement: &70175687156560 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70175687156560
|
58
|
+
description: ! 'ActiveModel-compliant form objects for rails app.
|
59
|
+
|
60
|
+
|
61
|
+
Forms have data and behavior. Let them be the
|
62
|
+
|
63
|
+
objects they want to be. Plus, get presentation-
|
64
|
+
|
65
|
+
specific validation logic out of your models.
|
66
|
+
|
67
|
+
'
|
68
|
+
email: satchmorun@gmail.com
|
69
|
+
executables: []
|
70
|
+
extensions: []
|
71
|
+
extra_rdoc_files: []
|
72
|
+
files:
|
73
|
+
- Gemfile
|
74
|
+
- LICENSE
|
75
|
+
- Rakefile
|
76
|
+
- README.md
|
77
|
+
- test/attributes.rb
|
78
|
+
- test/compliance.rb
|
79
|
+
- test/lint.rb
|
80
|
+
- test/nest_many.rb
|
81
|
+
- test/nest_one.rb
|
82
|
+
- test/rails.rb
|
83
|
+
- test/valid.rb
|
84
|
+
homepage: https://github.com/satchmorun/formality
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 1.8.10
|
106
|
+
signing_key:
|
107
|
+
specification_version: 3
|
108
|
+
summary: Form objects for your rails app.
|
109
|
+
test_files:
|
110
|
+
- test/attributes.rb
|
111
|
+
- test/compliance.rb
|
112
|
+
- test/lint.rb
|
113
|
+
- test/nest_many.rb
|
114
|
+
- test/nest_one.rb
|
115
|
+
- test/rails.rb
|
116
|
+
- test/valid.rb
|