organ 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +321 -0
- data/Rakefile +7 -0
- data/lib/organ.rb +7 -0
- data/lib/organ/coercer.rb +136 -0
- data/lib/organ/form.rb +131 -0
- data/lib/organ/validation_error.rb +20 -0
- data/lib/organ/validations.rb +161 -0
- data/organ.gemspec +21 -0
- metadata +53 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8033a1f23ace0b7e8ba5b04ecbf34b842f6bb1bc
|
4
|
+
data.tar.gz: edc103641da1a031b8ffec33070aa49ae88fd13a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ce1a123bbb939f0f28efbade808bcefd58a7151fdab8ebbc2588e533d03fb5d3f9c496f2be01a6cd9013ff26286b992b4f60c69d11163e85340bb4e3b8938ab9
|
7
|
+
data.tar.gz: cd965088fc635e7421658cffda80367d80be5d17fc94b64a46e9943e7eb8b14a34386de8c2326df7cfc1973c11f4172c359fb1fe737437ec2b89edd5abdb168d
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Sebastian Borrazas
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
Organ
|
2
|
+
=====
|
3
|
+
|
4
|
+
Forms with integrated validations and attribute coercing.
|
5
|
+
|
6
|
+
Introduction
|
7
|
+
-----------
|
8
|
+
|
9
|
+
Organ is a small library for manipulating form-based data with validations
|
10
|
+
attributes coercion.
|
11
|
+
|
12
|
+
These forms are very useful for handling HTTP requests where we receive certain
|
13
|
+
parameters and need to coerce them, validate them and then do something with
|
14
|
+
them. They are made so that the system has 1 form per service, so the form
|
15
|
+
names should usually be very explicit of the service they are providing
|
16
|
+
(`CreateUser`, `DeleteUser`, `SendTweet`, `NotifyAdmin`).
|
17
|
+
|
18
|
+
You should reuse forms behaviour (including validations) through inheritance
|
19
|
+
or modules (like having `CreateUser` inherit from `UpdateUser`). They can be
|
20
|
+
extended to handle worker jobs, to act as presenters, etc.
|
21
|
+
|
22
|
+
They do not handle HTML rendering of forms or do any HTTP manipulation.
|
23
|
+
|
24
|
+
The name `Organ` was inspired by the fact that organs beheave in a module manner
|
25
|
+
so that they do one thing only.
|
26
|
+
|
27
|
+
Usage
|
28
|
+
-----
|
29
|
+
|
30
|
+
A form is simply a class which inherits from `Organ::Form`. You can specify
|
31
|
+
the attributes the form will have with the `attributes` class method.
|
32
|
+
|
33
|
+
The `attributes` class method takes the attribute name and any options that
|
34
|
+
attribute has.
|
35
|
+
|
36
|
+
The options can be:
|
37
|
+
|
38
|
+
* `:type` - The type for which that attribute will be coerced.
|
39
|
+
* `:skip` - If `true` it won't include the attribute when calling the
|
40
|
+
`#attributes` method on the instance.
|
41
|
+
* `:skip_reader` - If `true`, it won't create the attribute reader for that
|
42
|
+
attribute.
|
43
|
+
|
44
|
+
Example:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class CreateCustomer < Organ::Form
|
48
|
+
|
49
|
+
attribute(:name, :type => :string, :trim => true)
|
50
|
+
attribute(:address, :type => :string, :trim => true)
|
51
|
+
|
52
|
+
def validate
|
53
|
+
validate_presence(:name)
|
54
|
+
validate_length(:name, :min => 4, :max => 255)
|
55
|
+
validate_length(:address, :max => 255)
|
56
|
+
|
57
|
+
validate_uniqueness(:address) do |addr|
|
58
|
+
Customer.where(:address => addr).empty?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def perform
|
63
|
+
Customer.create(attributes)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
# Sinatra example
|
69
|
+
post "/customer" do
|
70
|
+
form = CreateCustomer.new(params[:customer])
|
71
|
+
content_type(:json)
|
72
|
+
if form.valid?
|
73
|
+
form.perform
|
74
|
+
status(204)
|
75
|
+
else
|
76
|
+
status(422)
|
77
|
+
JSON.generate("errors" => form.errors)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Default types
|
83
|
+
-------------
|
84
|
+
|
85
|
+
The default types you can use are:
|
86
|
+
|
87
|
+
### :string
|
88
|
+
|
89
|
+
Coerces the value into a string or nil if no value given. If the `:trim` option
|
90
|
+
is given it also strips the preceding/trailing whitespaces and newlines.
|
91
|
+
|
92
|
+
### :boolean
|
93
|
+
|
94
|
+
Coerces the value into false (if no value given) or true otherwise.
|
95
|
+
|
96
|
+
### :array
|
97
|
+
|
98
|
+
Coerces the value into an Array. If it can't be coerced into an Array, it
|
99
|
+
returns an empty Array. An additional `:element_type` option with another type
|
100
|
+
can be specifed to coerce all the elements of the array into it.
|
101
|
+
|
102
|
+
If a Hash is passed instead of an array, it takes the Hash values.
|
103
|
+
|
104
|
+
### :float
|
105
|
+
|
106
|
+
Coerce the value into a Float, or nil of the value can't be coerced into a
|
107
|
+
float.
|
108
|
+
|
109
|
+
### :hash
|
110
|
+
|
111
|
+
Coerces the value into a Hash. If it can't be coerced into a Hash, it returns
|
112
|
+
an empty Hash. An additional `:key_type` and/or `:value_type` can be specified
|
113
|
+
to coerce the keys/values of the hash respectively.
|
114
|
+
|
115
|
+
### :integer
|
116
|
+
|
117
|
+
Coerces the value into a Fixnum. If it can't be coerced it returns nil.
|
118
|
+
|
119
|
+
### :date
|
120
|
+
|
121
|
+
Coerces the value into a date. If the value doesn't have the `%Y-%m-%d` format
|
122
|
+
it returns nil.
|
123
|
+
|
124
|
+
Default validations
|
125
|
+
-------------------
|
126
|
+
|
127
|
+
### validate_presence
|
128
|
+
|
129
|
+
If the value is falsy or an empty string it appends a `:blank` error to the
|
130
|
+
attribute.
|
131
|
+
|
132
|
+
### validate_uniqueness
|
133
|
+
|
134
|
+
If the value is present and the block passed returns false, it appends a
|
135
|
+
`:taken` error to the attribute. Example:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
validate_uniqueness(:username) do |username|
|
139
|
+
User.where(:username => username).empty?
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### validate_email_format
|
144
|
+
|
145
|
+
If the value is present and doesn't match an emails format, it appends an
|
146
|
+
`:invalid` error to the attribute.
|
147
|
+
|
148
|
+
### validate_format
|
149
|
+
|
150
|
+
If the value is present and doesn't match the specified format, it appends an
|
151
|
+
`:invalid` error to the attribute.
|
152
|
+
|
153
|
+
### validate_length
|
154
|
+
|
155
|
+
If the value is present and shorter than the `:min` option, it appends a
|
156
|
+
`:too_short` error to the attribute. If it's longer than the `:max` option, it
|
157
|
+
appends a `:too_long` error to the attribute. Example:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
validate_length(:username, :min => 3, :max => 255)
|
161
|
+
validate_length(:first_name, :max => 255)
|
162
|
+
```
|
163
|
+
|
164
|
+
### validate_inclusion
|
165
|
+
|
166
|
+
If the value is present and not included on the given list it appends a
|
167
|
+
`:not_included` error to the attribute.
|
168
|
+
|
169
|
+
### validate_range
|
170
|
+
|
171
|
+
If the value is present and less than the `:min` option, it appends a
|
172
|
+
`:less_than` error to the attribute. If it's greater than the `:max` option, it
|
173
|
+
appends a `:greater_than` error to the attribute. Example:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
validate_range(:age, :min => 18)
|
177
|
+
```
|
178
|
+
|
179
|
+
Extensions
|
180
|
+
----------
|
181
|
+
|
182
|
+
These forms were meant to be extended when necessary. These are a few examples
|
183
|
+
of how they can be extended.
|
184
|
+
|
185
|
+
### Extensions::Paginate
|
186
|
+
|
187
|
+
An extension to paginate results with Sequel Datasets.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
module Extensions
|
191
|
+
module Presenter
|
192
|
+
|
193
|
+
DEFAULT_PER_PAGE = 30
|
194
|
+
MAX_PER_PAGE = 100
|
195
|
+
|
196
|
+
def self.included(base)
|
197
|
+
base.attribute(:page, :type => :integer, :skip_reader => true)
|
198
|
+
base.attribute(:per_page, :type => :integer, :skip_reader => true)
|
199
|
+
end
|
200
|
+
|
201
|
+
def each(&block)
|
202
|
+
results.each(&block)
|
203
|
+
end
|
204
|
+
|
205
|
+
def total_pages
|
206
|
+
@total_pages ||= (1.0 * dataset.count / per_page).ceil
|
207
|
+
end
|
208
|
+
|
209
|
+
def any?
|
210
|
+
total_pages > 0
|
211
|
+
end
|
212
|
+
|
213
|
+
def results
|
214
|
+
@results ||= begin
|
215
|
+
start = (page - 1) * per_page
|
216
|
+
_dataset = dataset.limit(per_page, start)
|
217
|
+
_dataset.all
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def per_page
|
222
|
+
if @per_page && @per_page >= 1 && per_page <= MAX_PER_PAGE
|
223
|
+
@per_page
|
224
|
+
else
|
225
|
+
DEFAULT_PER_PAGE
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def page
|
230
|
+
@page && @page > 1 ? @page : 1
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
module Presenters
|
237
|
+
class UserPets < Organ::Form
|
238
|
+
|
239
|
+
include Extensions::Paginate
|
240
|
+
|
241
|
+
attribute(:user_id, :type => :integer)
|
242
|
+
|
243
|
+
def dataset
|
244
|
+
Pet.where(:user_id => user_id)
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Sinatra app
|
251
|
+
get "/pets" do
|
252
|
+
presenter = Presenter::UserPets.new({
|
253
|
+
:user_id => session[:user_id],
|
254
|
+
:page => params[:page],
|
255
|
+
:per_page => params[:per_page]
|
256
|
+
})
|
257
|
+
erb(:"pets/index", :locals => { :pets => presenter })
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
### Extensions::Worker
|
262
|
+
|
263
|
+
An extension to create worker job handlers using
|
264
|
+
[Ost](https://github.com/soveran/ost).
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
module Extensions
|
268
|
+
module Worker
|
269
|
+
|
270
|
+
def self.queue_job(attributes)
|
271
|
+
queue << JSON.generate(attributes)
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.stop
|
275
|
+
queue.stop
|
276
|
+
end
|
277
|
+
|
278
|
+
def self.watch_queue
|
279
|
+
queue.each do |json_str|
|
280
|
+
attributes = JSON.parse(json_str)
|
281
|
+
new(attributes).perform
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def self.queue
|
288
|
+
Ost[self.name]
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
module Workers
|
295
|
+
class EmailNotifier < Organ::Form
|
296
|
+
|
297
|
+
include Extensions::Worker
|
298
|
+
|
299
|
+
attribute(:email)
|
300
|
+
attribute(:message)
|
301
|
+
|
302
|
+
def perform
|
303
|
+
# send message to email...
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Sinatra app
|
310
|
+
get "/queue_email" do
|
311
|
+
Workers::EmailNotifier.queue_job(params[:notification])
|
312
|
+
status(204)
|
313
|
+
end
|
314
|
+
```
|
315
|
+
|
316
|
+
Aknowledgements
|
317
|
+
---------------
|
318
|
+
|
319
|
+
This library was inspired mainly by @soveran
|
320
|
+
[Scrivener](https://github.com/soveran/scrivener) and was made with the help ofn
|
321
|
+
@grilix.
|
data/Rakefile
ADDED
data/lib/organ.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module Organ
|
2
|
+
module Coercer
|
3
|
+
|
4
|
+
# Coerce the value into a String or nil if no value given.
|
5
|
+
#
|
6
|
+
# @param value [Object]
|
7
|
+
# @option options [Boolan] :trim (false)
|
8
|
+
# If true, it strips the preceding/trailing whitespaces and newlines.
|
9
|
+
# It also replaces multiple consecutive spaces into one.
|
10
|
+
#
|
11
|
+
# @return [String, nil]
|
12
|
+
#
|
13
|
+
# @api semipublic
|
14
|
+
def coerce_string(value, options = {})
|
15
|
+
value = value ? value.to_s : nil
|
16
|
+
if value && options[:trim]
|
17
|
+
value = value.strip.gsub(/\s{2,}/, " ")
|
18
|
+
end
|
19
|
+
value
|
20
|
+
end
|
21
|
+
|
22
|
+
# Corce the value into true or false.
|
23
|
+
#
|
24
|
+
# @param value [Object]
|
25
|
+
#
|
26
|
+
# @return [Boolean]
|
27
|
+
#
|
28
|
+
# @api semipublic
|
29
|
+
def coerce_boolean(value, options = {})
|
30
|
+
!!value
|
31
|
+
end
|
32
|
+
|
33
|
+
# Coerce the value into an Array.
|
34
|
+
#
|
35
|
+
# @param value [Object]
|
36
|
+
# The value to be coerced.
|
37
|
+
# @option options [Symbol] :element_type (nil)
|
38
|
+
# The type of the value to coerce each element of the array. No coercion
|
39
|
+
# done if type is nil.
|
40
|
+
#
|
41
|
+
# @return [Array]
|
42
|
+
# The coerced Array.
|
43
|
+
#
|
44
|
+
# @api semipublic
|
45
|
+
def coerce_array(value, options = {})
|
46
|
+
element_type = options[:element_type]
|
47
|
+
value = value.values if value.kind_of?(Hash)
|
48
|
+
if value.kind_of?(Array)
|
49
|
+
if element_type
|
50
|
+
value.map do |array_element|
|
51
|
+
send("coerce_#{element_type}", array_element)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
value
|
55
|
+
end
|
56
|
+
else
|
57
|
+
[]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Coerce the value into a Float.
|
62
|
+
#
|
63
|
+
# @param value [Object]
|
64
|
+
#
|
65
|
+
# @return [Float, nil]
|
66
|
+
#
|
67
|
+
# @api semipublic
|
68
|
+
def coerce_float(value, options = {})
|
69
|
+
Float(value) rescue nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# Coerce the value into a Hash.
|
73
|
+
#
|
74
|
+
# @param value [Object]
|
75
|
+
# The value to be coerced.
|
76
|
+
# @option options :key_type [Symbol] (nil)
|
77
|
+
# The type of the hash keys to coerce, no coersion done if type is nil.
|
78
|
+
# @options options :value_type [Symbol] (nil)
|
79
|
+
# The type of the hash values to coerce, no coersion done if type is nil.
|
80
|
+
#
|
81
|
+
# @return [Hash]
|
82
|
+
# The coerced Hash.
|
83
|
+
#
|
84
|
+
# @api semipublic
|
85
|
+
def coerce_hash(value, options = {})
|
86
|
+
key_type = options[:key_type]
|
87
|
+
value_type = options[:value_type]
|
88
|
+
if value.kind_of?(Hash)
|
89
|
+
value.each_with_object({}) do |(key, value), coerced_hash|
|
90
|
+
key = send("coerce_#{key_type}", key) if key_type
|
91
|
+
value = send("coerce_#{value_type}", value) if value_type
|
92
|
+
coerced_hash[key] = value
|
93
|
+
end
|
94
|
+
else
|
95
|
+
{}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Coerce the value into an Integer.
|
100
|
+
#
|
101
|
+
# @param value [Object]
|
102
|
+
# The value to be coerced.
|
103
|
+
#
|
104
|
+
# @return [Integer, nil]
|
105
|
+
# An Integer if the value can be coerced or nil otherwise.
|
106
|
+
#
|
107
|
+
# @api semipublic
|
108
|
+
def coerce_integer(value, options = {})
|
109
|
+
value = value.to_s
|
110
|
+
if value.match(/\A0|[1-9]\d*\z/)
|
111
|
+
value.to_i
|
112
|
+
else
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Coerce the value into a Date.
|
118
|
+
#
|
119
|
+
# @param value [Object]
|
120
|
+
# The value to be coerced.
|
121
|
+
#
|
122
|
+
# @return [Date, nil]
|
123
|
+
# A Date if the value can be coerced or nil otherwise.
|
124
|
+
#
|
125
|
+
# @api semipublic
|
126
|
+
def coerce_date(value, options = {})
|
127
|
+
value = coerce(value, String)
|
128
|
+
begin
|
129
|
+
Date.strptime(value, "%Y-%m-%d")
|
130
|
+
rescue ArgumentError
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
data/lib/organ/form.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require_relative "validation_error"
|
2
|
+
require_relative "validations"
|
3
|
+
require_relative "coercer"
|
4
|
+
|
5
|
+
module Organ
|
6
|
+
# Form for doing actions based on the attributes specified.
|
7
|
+
# This class has to be inherited by different forms, each performing a
|
8
|
+
# different action. If validations are needed, #validate method should be
|
9
|
+
# overridden.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# class LoginForm < Organ::Form
|
14
|
+
#
|
15
|
+
# attribute(:username, :type => :string)
|
16
|
+
# attribute(:password, :type => :string)
|
17
|
+
#
|
18
|
+
# def validate
|
19
|
+
# unless valid_login?
|
20
|
+
# append_error(:username, :invalid)
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# private
|
25
|
+
#
|
26
|
+
# def valid_login?
|
27
|
+
# user = User.where(username: username).first
|
28
|
+
# user && check_password_secure(user.password, password)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
class Form
|
34
|
+
|
35
|
+
include Organ::Validations
|
36
|
+
include Organ::Coercer
|
37
|
+
|
38
|
+
# Copy parent attributes to inherited class.
|
39
|
+
#
|
40
|
+
# @param klass [Class]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def self.inherited(klass)
|
44
|
+
super(klass)
|
45
|
+
klass.instance_variable_set(:@attributes, attributes.dup)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Define an attribute for the form.
|
49
|
+
#
|
50
|
+
# @param name
|
51
|
+
# The Symbol attribute name.
|
52
|
+
# @option options [Symbol] :type (nil)
|
53
|
+
# The type of this attribute.
|
54
|
+
# @option options [Symbol] :skip_reader (false)
|
55
|
+
# If true, skips from creating the attribute reader.
|
56
|
+
#
|
57
|
+
# @api public
|
58
|
+
def self.attribute(name, options = {})
|
59
|
+
attr_reader(name) unless options[:skip_reader]
|
60
|
+
define_method("#{name}=") do |value|
|
61
|
+
if options[:type]
|
62
|
+
value = send("coerce_#{options[:type]}", value, options)
|
63
|
+
end
|
64
|
+
instance_variable_set("@#{name}", value)
|
65
|
+
end
|
66
|
+
attributes[name] = options
|
67
|
+
end
|
68
|
+
|
69
|
+
# Retrieve the list of attributes of the form.
|
70
|
+
#
|
71
|
+
# @return [Hash]
|
72
|
+
# The class attributes hash.
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
def self.attributes
|
76
|
+
@attributes ||= {}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Initialize a new Form::Base form.
|
80
|
+
#
|
81
|
+
# @param attrs [Hash]
|
82
|
+
# The attributes values to use for the new instance.
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def initialize(attrs = {})
|
86
|
+
set_attributes(attrs)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Set the attributes belonging to the form.
|
90
|
+
#
|
91
|
+
# @param attrs [Hash<String, Object>]
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
def set_attributes(attrs)
|
95
|
+
return unless attrs.kind_of?(Hash)
|
96
|
+
|
97
|
+
attrs = coerce_hash(attrs, :key_type => :string)
|
98
|
+
|
99
|
+
self.class.attributes.each do |attribute_name, _|
|
100
|
+
send("#{attribute_name}=", attrs[attribute_name.to_s])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Get the all attributes with its values of the current form.
|
105
|
+
#
|
106
|
+
# @return [Hash<Symbol, Object>]
|
107
|
+
#
|
108
|
+
# @api public
|
109
|
+
def attributes
|
110
|
+
self.class.attributes.each_with_object({}) do |(name, opts), attrs|
|
111
|
+
attrs[name] = send(name) unless opts[:skip]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Validate and perform the form actions. If any errors come up during the
|
116
|
+
# validation or the #perform method, raise an exception with the errors.
|
117
|
+
#
|
118
|
+
# @raise [Organ::ValidationError]
|
119
|
+
#
|
120
|
+
# @api public
|
121
|
+
def perform!
|
122
|
+
if valid?
|
123
|
+
perform
|
124
|
+
end
|
125
|
+
if errors.any?
|
126
|
+
raise Organ::ValidationError.new(errors)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Organ
|
2
|
+
class ValidationError < StandardError
|
3
|
+
|
4
|
+
# !@attribute [r] errors
|
5
|
+
# @return [Hash<Symbol, Array>]
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
attr_reader :errors
|
9
|
+
|
10
|
+
# Initialize the Organ::ValidationError.
|
11
|
+
#
|
12
|
+
# @param errors [Hash<Symbol, Array>]
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def initialize(errors)
|
16
|
+
@errors = errors
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module Organ
|
2
|
+
module Validations
|
3
|
+
|
4
|
+
EMAIL_FORMAT = /\A
|
5
|
+
([0-9a-zA-Z\.][-\w\+\.]*)@
|
6
|
+
([0-9a-zA-Z_][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9}\z/x
|
7
|
+
|
8
|
+
# Get the current form errors Hash.
|
9
|
+
#
|
10
|
+
# @return [Hash<Symbol, Array<Symbol>]
|
11
|
+
# The errors Hash, having the Symbol attribute names as keys and an
|
12
|
+
# array of errors (Symbols) as the value.
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def errors
|
16
|
+
@errors ||= Hash.new { |hash, key| hash[key] = [] }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Determine if current form instance is valid by running the validations
|
20
|
+
# specified on #validate.
|
21
|
+
#
|
22
|
+
# @return [Boolean]
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
def valid?
|
26
|
+
errors.clear
|
27
|
+
validate
|
28
|
+
errors.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Append an error to the given attribute.
|
32
|
+
#
|
33
|
+
# @param attribute_name [Symbol]
|
34
|
+
# @param error [Symbol]
|
35
|
+
# The error identifier.
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def append_error(attribute_name, error)
|
39
|
+
errors[attribute_name] << error
|
40
|
+
end
|
41
|
+
|
42
|
+
# Validate the presence of the attribute value. If the value is nil or
|
43
|
+
# false append a :blank error to the attribute.
|
44
|
+
#
|
45
|
+
# @param attribute_name [Symbol]
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def validate_presence(attribute_name)
|
49
|
+
value = send(attribute_name)
|
50
|
+
if !value || value.to_s.empty?
|
51
|
+
append_error(attribute_name, :blank)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Validate the uniqueness of the attribute value. The uniqueness is
|
56
|
+
# determined by the block given. If the value is not unique, append the
|
57
|
+
# :taken error to the attribute.
|
58
|
+
#
|
59
|
+
# @param attribute_name [Symbol]
|
60
|
+
# @param block [Proc]
|
61
|
+
# A block to determine if a given value is unique or not. It receives
|
62
|
+
# the value and should return true if the value is unique.
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
def validate_uniqueness(attribute_name, &block)
|
66
|
+
value = send(attribute_name)
|
67
|
+
unless block.call(value)
|
68
|
+
append_error(attribute_name, :taken)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Validate the email format. If the value does not match the email format,
|
73
|
+
# append the :invalid error to the attribute.
|
74
|
+
#
|
75
|
+
# @param attribute_name [Symbol]
|
76
|
+
#
|
77
|
+
# @api public
|
78
|
+
def validate_email_format(attribute_name)
|
79
|
+
validate_format(attribute_name, EMAIL_FORMAT)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Validate the format of the attribute value. If the value does not match
|
83
|
+
# the regexp given, append :invalid error to the attribute.
|
84
|
+
#
|
85
|
+
# @param attribute_name [Symbol]
|
86
|
+
# @param format [Regexp]
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def validate_format(attribute_name, format)
|
90
|
+
value = send(attribute_name)
|
91
|
+
if value && !(format =~ value)
|
92
|
+
append_error(attribute_name, :invalid)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Validate the length of a String, Array or any other form attribute which
|
97
|
+
# responds to #size. If the value is too short, append the :too_short
|
98
|
+
# error to the attribute. If the value is too long append the :too_long
|
99
|
+
# error to the attribute.
|
100
|
+
#
|
101
|
+
# @param attribute_name [Symbol]
|
102
|
+
# @option options [Integer, nil] :min (nil)
|
103
|
+
# @option options [Integer, nil] :max (nil)
|
104
|
+
#
|
105
|
+
# @api public
|
106
|
+
def validate_length(attribute_name, options = {})
|
107
|
+
min = options.fetch(:min, nil)
|
108
|
+
max = options.fetch(:max, nil)
|
109
|
+
value = send(attribute_name)
|
110
|
+
|
111
|
+
if value
|
112
|
+
length = value.size
|
113
|
+
if min && length < min
|
114
|
+
append_error(attribute_name, :too_short)
|
115
|
+
end
|
116
|
+
if max && length > max
|
117
|
+
append_error(attribute_name, :too_long)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Validate the value of the given attribute is included in the list. If
|
123
|
+
# the value is not included in the list, append the :not_included error to
|
124
|
+
# the attribute.
|
125
|
+
#
|
126
|
+
# @param attribute_name [Symbol]
|
127
|
+
# @param attribute_name [Symbol]
|
128
|
+
# @param list [Array]
|
129
|
+
#
|
130
|
+
# @api public
|
131
|
+
def validate_inclusion(attribute_name, list)
|
132
|
+
value = send(attribute_name)
|
133
|
+
if value && !list.include?(value)
|
134
|
+
append_error(attribute_name, :not_included)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Validate the range in which the attribute can be. If the value is less
|
139
|
+
# than the min a :less_than_min error will be appended. If the value is
|
140
|
+
# greater than the max a :greater_than_max error will be appended.
|
141
|
+
#
|
142
|
+
# @param attribute_name [Symbol]
|
143
|
+
# @option options [Integer] :min (nil)
|
144
|
+
# The minimum value the attribute can take, if nil, no validation is made.
|
145
|
+
# @option options [Integer] :max (nil)
|
146
|
+
# The maximum value the attribute can take, if nil, no validation is made.
|
147
|
+
#
|
148
|
+
# @api public
|
149
|
+
def validate_range(attribute_name, options = {})
|
150
|
+
value = send(attribute_name)
|
151
|
+
|
152
|
+
return unless value
|
153
|
+
|
154
|
+
min = options.fetch(:min, nil)
|
155
|
+
max = options.fetch(:max, nil)
|
156
|
+
append_error(attribute_name, :less_than) if min && value < min
|
157
|
+
append_error(attribute_name, :greater_than) if max && value > max
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
data/organ.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "organ"
|
3
|
+
s.version = "0.0.1"
|
4
|
+
s.summary = "Forms with integrated validations and attribute coercing."
|
5
|
+
s.description = "A small library for manipulating form-based data with validations and attributes coercion."
|
6
|
+
s.authors = ["Sebastian Borrazas"]
|
7
|
+
s.email = ["seba.borrazas@gmail.com"]
|
8
|
+
s.homepage = "http://github.com/sborrazas/organ"
|
9
|
+
|
10
|
+
s.files = Dir[
|
11
|
+
"LICENSE",
|
12
|
+
"CHANGELOG",
|
13
|
+
"README.md",
|
14
|
+
"Rakefile",
|
15
|
+
"lib/**/*.rb",
|
16
|
+
"*.gemspec",
|
17
|
+
"test/*.*"
|
18
|
+
]
|
19
|
+
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: organ
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Borrazas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-14 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A small library for manipulating form-based data with validations and
|
14
|
+
attributes coercion.
|
15
|
+
email:
|
16
|
+
- seba.borrazas@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- LICENSE
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- lib/organ.rb
|
25
|
+
- lib/organ/coercer.rb
|
26
|
+
- lib/organ/form.rb
|
27
|
+
- lib/organ/validation_error.rb
|
28
|
+
- lib/organ/validations.rb
|
29
|
+
- organ.gemspec
|
30
|
+
homepage: http://github.com/sborrazas/organ
|
31
|
+
licenses: []
|
32
|
+
metadata: {}
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options: []
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 2.2.0
|
50
|
+
signing_key:
|
51
|
+
specification_version: 4
|
52
|
+
summary: Forms with integrated validations and attribute coercing.
|
53
|
+
test_files: []
|