formality 0.0.1
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.
- 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
|