lotus-validations 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 36c1599d9dc59f556a6041f459b41fe23a629e12
4
- data.tar.gz: 16fade6ce990dfa2dc5f6bd477dae2bdf0160ab5
3
+ metadata.gz: 0249c7c704d709f9579693a8f8b8faac3c1ca338
4
+ data.tar.gz: e2913b58194002b58ec6da05154982e46d30efd5
5
5
  SHA512:
6
- metadata.gz: b53e4dfe4cc649cd1f7818d00b5de4a8171d6a0ffded3b12e603497abdbec84a31d97d062b87ed38b130e314beef3b06408a4632f957ba257823b9a4bafb22dc
7
- data.tar.gz: 32dfdd1ed995e28ce04b631c2f9ee367347c7dfdf05936e9771f2c1254ea0aad763a91c62e0921c4ad9433487f5259bfb71e24a1133f7a7011aed78c9fa604b2
6
+ metadata.gz: e82fe5c215943d1fdfd11068c1b5639dcec94062a1e1cce5dafee5ee643e09dde7b05b826f1c32d7d9c10e33a0b618587cdae1a393c21f0361388f8162798afa
7
+ data.tar.gz: b92988e71b73a27e7319acf7957e001042035ce9091c3f2a1e55d83f7f9d4afb45f6c41c9742d1ba663e4eca107977c714685e53577143b7baf5e51bc11ad4b7
data/CHANGELOG.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Lotus::Validations
2
2
  Validations mixin for Ruby objects
3
3
 
4
+ ## v0.2.2 - 2015-01-08
5
+ ### Added
6
+ - [Steve Hodgkiss] Introduced `Validations.validates`. It defines validations, for already existing attributes.
7
+
4
8
  ## v0.2.1 - 2014-12-23
5
9
  ### Added
6
10
  - [Luca Guidi] Introduced `Validations::Errors#to_h` and `to_a`
7
11
  - [Luca Guidi] Introduced `Validations::Errors#any?`
12
+ - [Luca Guidi] Official support for Ruby 2.2
8
13
 
9
14
  ### Fixed
10
15
  - [Satoshi Amemiya] Made `Validations#valid?` idempotent
data/README.md CHANGED
@@ -57,14 +57,46 @@ require 'lotus/validations'
57
57
  class Person
58
58
  include Lotus::Validations
59
59
 
60
- attribute :name
60
+ attribute :name, presence: true
61
+ attribute :email, presence: true
61
62
  end
62
63
 
63
- person = Person.new(name: 'Luca', age: 32)
64
- person.name # => "Luca"
65
- person.age # => raises NoMethodError because `:age` wasn't defined as attribute.
64
+ person = Person.new(name: 'Luca', email: 'me@example.org', age: 32)
65
+ person.name # => "Luca"
66
+ person.email # => "me@example.org"
67
+ person.age # => raises NoMethodError because `:age` wasn't defined as attribute.
66
68
  ```
67
69
 
70
+ ### Validations
71
+
72
+ If you prefer Lotus::Validations to **only define validations**, but **not attributes**,
73
+ you can use the following alternative syntax.
74
+
75
+ ```ruby
76
+ require 'lotus/validations'
77
+
78
+ class Person
79
+ include Lotus::Validations
80
+ attr_accessor :name, :email
81
+
82
+ # Custom initializer
83
+ def initialize(attributes = {})
84
+ @name, @email = attributes.values_at(:name, :email)
85
+ end
86
+
87
+ validates :name, presence: true
88
+ validates :email, presence: true
89
+ end
90
+
91
+ person = Person.new(name: 'Luca', email: 'me@example.org')
92
+ person.name # => "Luca"
93
+ person.email # => "me@example.org"
94
+ ```
95
+
96
+ This is a bit more verbose, but offers a great level of flexibility for your
97
+ Ruby objects. It also allows to use Lotus::Validations in combination with
98
+ **other frameworks**.
99
+
68
100
  ### Coercions
69
101
 
70
102
  If a Ruby class is passed to the `:type` option, the given value is coerced, accordingly.
@@ -129,7 +161,7 @@ end
129
161
  person = Person.new(fav_number: '23', date: 'Oct 23, 2014')
130
162
  person.valid?
131
163
 
132
- person.fav_number # => 23
164
+ person.fav_number # => #<FavNumber:0x007ffc644bba00 @number="23">
133
165
  person.date # => this raises an error, because BirthDate#initialize doesn't accept any arg
134
166
  ```
135
167
 
@@ -24,10 +24,6 @@ module Lotus
24
24
  # @api private
25
25
  CONFIRMATION_TEMPLATE = '%{name}_confirmation'.freeze
26
26
 
27
- # @since 0.2.0
28
- # @api private
29
- BLANK_STRING_MATCHER = /\A[[:space:]]*\z/.freeze
30
-
31
27
  # Instantiate an attribute
32
28
  #
33
29
  # @param attributes [Hash] a set of attributes and values coming from the
@@ -50,23 +46,13 @@ module Lotus
50
46
  # @since 0.2.0
51
47
  def validate
52
48
  _with_cleared_errors do
53
- presence
54
- acceptance
55
-
56
49
  _run_validations
57
50
  end
58
51
  end
59
52
 
60
53
  # @api private
61
54
  # @since 0.2.0
62
- def value
63
- if (coercer = @validations[:type])
64
- return nil if blank_value?
65
- Lotus::Validations::Coercions.coerce(coercer, @value)
66
- else
67
- @value
68
- end
69
- end
55
+ attr_reader :value
70
56
 
71
57
  private
72
58
  # Validates presence of the value.
@@ -197,71 +183,20 @@ module Lotus
197
183
  end
198
184
  end
199
185
 
200
- # Coerces the value to the defined type.
201
- # Built in types are:
202
- #
203
- # * `Array`
204
- # * `BigDecimal`
205
- # * `Boolean`
206
- # * `Date`
207
- # * `DateTime`
208
- # * `Float`
209
- # * `Hash`
210
- # * `Integer`
211
- # * `Pathname`
212
- # * `Set`
213
- # * `String`
214
- # * `Symbol`
215
- # * `Time`
216
- #
217
- # If a user defined class is specified, it can be freely used for coercion
218
- # purposes. The only limitation is that the constructor should have
219
- # **arity of 1**.
220
- #
221
- # @raise [ArgumentError] if the custom coercer's `#initialize` has a wrong arity.
222
- #
223
- # @see Lotus::Validations::ClassMethods#attribute
224
- # @see Lotus::Validations::Coercions
225
- #
226
- # @since 0.2.0
227
- # @api private
228
- def coerce
229
- _validate(:type) do |coercer|
230
- Lotus::Validations::Coercions.coerce(coercer, @value)
231
- true
232
- end
233
- end
234
-
235
- # Checks if the value is `nil`.
236
- #
237
- # @since 0.2.0
186
+ # @since 0.1.0
238
187
  # @api private
239
- def nil_value?
188
+ def skip?
240
189
  @value.nil?
241
190
  end
242
191
 
243
- alias_method :skip?, :nil_value?
244
-
245
192
  # Checks if the value is "blank".
246
193
  #
247
- # @see Lotus::Validations::Attribute#presence
194
+ # @see Lotus::Validations::BlankValueChecker
248
195
  #
249
196
  # @since 0.2.0
250
197
  # @api private
251
198
  def blank_value?
252
- nil_value? || _blank_string? || _empty_value?
253
- end
254
-
255
- # @since 0.2.0
256
- # @api private
257
- def _blank_string?
258
- (@value.respond_to?(:match) and @value.match(BLANK_STRING_MATCHER))
259
- end
260
-
261
- # @since 0.2.0
262
- # @api private
263
- def _empty_value?
264
- (@value.respond_to?(:empty?) and @value.empty?)
199
+ BlankValueChecker.new(@value).blank_value?
265
200
  end
266
201
 
267
202
  # Run the defined validations
@@ -269,10 +204,12 @@ module Lotus
269
204
  # @since 0.2.0
270
205
  # @api private
271
206
  def _run_validations
207
+ presence
208
+ acceptance
209
+
272
210
  return if skip?
273
211
 
274
212
  format
275
- coerce
276
213
  inclusion
277
214
  exclusion
278
215
  size
@@ -0,0 +1,354 @@
1
+ require 'set'
2
+ require 'lotus/utils/attributes'
3
+
4
+ module Lotus
5
+ module Validations
6
+ # Define attributes and their validations together
7
+ #
8
+ # @since 0.2.2
9
+ # @api private
10
+ module AttributeDefiner
11
+ # Override Ruby's hook for modules.
12
+ #
13
+ # @param base [Class] the target class
14
+ #
15
+ # @since 0.2.2
16
+ # @api private
17
+ #
18
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
19
+ def self.included(base)
20
+ base.extend ClassMethods
21
+ end
22
+
23
+ # @since 0.2.2
24
+ # @api private
25
+ module ClassMethods
26
+ # Override Ruby's hook for modules.
27
+ #
28
+ # @param base [Class] the target class
29
+ #
30
+ # @since 0.2.2
31
+ # @api private
32
+ #
33
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-extended
34
+ def included(base)
35
+ super
36
+ base.defined_attributes.merge(defined_attributes)
37
+ end
38
+
39
+ # Override Ruby's hook for modules.
40
+ #
41
+ # @param base [Class] the target class
42
+ #
43
+ # @since 0.2.2
44
+ # @api private
45
+ #
46
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-extended
47
+ def inherited(base)
48
+ super
49
+ base.defined_attributes.merge(defined_attributes)
50
+ end
51
+
52
+ # Define an attribute
53
+ #
54
+ # @param name [#to_sym] the name of the attribute
55
+ # @param options [Hash] optional set of validations
56
+ # @option options [Class] :type the Ruby type used to coerce the value
57
+ # @option options [TrueClass,FalseClass] :acceptance requires Ruby
58
+ # thruthiness of the value
59
+ # @option options [TrueClass,FalseClass] :confirmation requires the value
60
+ # to be confirmed twice
61
+ # @option options [#include?] :exclusion requires the value NOT be
62
+ # included in the given collection
63
+ # @option options [Regexp] :format requires value to match the given
64
+ # Regexp
65
+ # @option options [#include?] :inclusion requires the value BE included in
66
+ # the given collection
67
+ # @option options [TrueClass,FalseClass] :presence requires the value be
68
+ # included in the given collection
69
+ # @option options [Numeric,Range] :size requires value's to be equal or
70
+ # included by the given validator
71
+ #
72
+ # @raise [ArgumentError] if an unknown or mispelled validation is given
73
+ #
74
+ # @since 0.2.2
75
+ #
76
+ # @example Attributes
77
+ # require 'lotus/validations'
78
+ #
79
+ # class Person
80
+ # include Lotus::Validations
81
+ #
82
+ # attribute :name
83
+ # end
84
+ #
85
+ # person = Person.new(name: 'Luca', age: 32)
86
+ # person.name # => "Luca"
87
+ # person.age # => raises NoMethodError because `:age` wasn't defined as attribute.
88
+ #
89
+ # @example Standard coercions
90
+ # require 'lotus/validations'
91
+ #
92
+ # class Person
93
+ # include Lotus::Validations
94
+ #
95
+ # attribute :fav_number, type: Integer
96
+ # end
97
+ #
98
+ # person = Person.new(fav_number: '23')
99
+ # person.valid?
100
+ #
101
+ # person.fav_number # => 23
102
+ #
103
+ # @example Custom coercions
104
+ # require 'lotus/validations'
105
+ #
106
+ # class FavNumber
107
+ # def initialize(number)
108
+ # @number = number
109
+ # end
110
+ # end
111
+ #
112
+ # class BirthDate
113
+ # end
114
+ #
115
+ # class Person
116
+ # include Lotus::Validations
117
+ #
118
+ # attribute :fav_number, type: FavNumber
119
+ # attribute :date, type: BirthDate
120
+ # end
121
+ #
122
+ # person = Person.new(fav_number: '23', date: 'Oct 23, 2014')
123
+ # person.valid?
124
+ #
125
+ # person.fav_number # => 23
126
+ # person.date # => this raises an error, because BirthDate#initialize doesn't accept any arg
127
+ #
128
+ # @example Acceptance
129
+ # require 'lotus/validations'
130
+ #
131
+ # class Signup
132
+ # include Lotus::Validations
133
+ #
134
+ # attribute :terms_of_service, acceptance: true
135
+ # end
136
+ #
137
+ # signup = Signup.new(terms_of_service: '1')
138
+ # signup.valid? # => true
139
+ #
140
+ # signup = Signup.new(terms_of_service: '')
141
+ # signup.valid? # => false
142
+ #
143
+ # @example Confirmation
144
+ # require 'lotus/validations'
145
+ #
146
+ # class Signup
147
+ # include Lotus::Validations
148
+ #
149
+ # attribute :password, confirmation: true
150
+ # end
151
+ #
152
+ # signup = Signup.new(password: 'secret', password_confirmation: 'secret')
153
+ # signup.valid? # => true
154
+ #
155
+ # signup = Signup.new(password: 'secret', password_confirmation: 'x')
156
+ # signup.valid? # => false
157
+ #
158
+ # @example Exclusion
159
+ # require 'lotus/validations'
160
+ #
161
+ # class Signup
162
+ # include Lotus::Validations
163
+ #
164
+ # attribute :music, exclusion: ['pop']
165
+ # end
166
+ #
167
+ # signup = Signup.new(music: 'rock')
168
+ # signup.valid? # => true
169
+ #
170
+ # signup = Signup.new(music: 'pop')
171
+ # signup.valid? # => false
172
+ #
173
+ # @example Format
174
+ # require 'lotus/validations'
175
+ #
176
+ # class Signup
177
+ # include Lotus::Validations
178
+ #
179
+ # attribute :name, format: /\A[a-zA-Z]+\z/
180
+ # end
181
+ #
182
+ # signup = Signup.new(name: 'Luca')
183
+ # signup.valid? # => true
184
+ #
185
+ # signup = Signup.new(name: '23')
186
+ # signup.valid? # => false
187
+ #
188
+ # @example Inclusion
189
+ # require 'lotus/validations'
190
+ #
191
+ # class Signup
192
+ # include Lotus::Validations
193
+ #
194
+ # attribute :age, inclusion: 18..99
195
+ # end
196
+ #
197
+ # signup = Signup.new(age: 32)
198
+ # signup.valid? # => true
199
+ #
200
+ # signup = Signup.new(age: 17)
201
+ # signup.valid? # => false
202
+ #
203
+ # @example Presence
204
+ # require 'lotus/validations'
205
+ #
206
+ # class Signup
207
+ # include Lotus::Validations
208
+ #
209
+ # attribute :name, presence: true
210
+ # end
211
+ #
212
+ # signup = Signup.new(name: 'Luca')
213
+ # signup.valid? # => true
214
+ #
215
+ # signup = Signup.new(name: nil)
216
+ # signup.valid? # => false
217
+ #
218
+ # @example Size
219
+ # require 'lotus/validations'
220
+ #
221
+ # class Signup
222
+ # MEGABYTE = 1024 ** 2
223
+ # include Lotus::Validations
224
+ #
225
+ # attribute :ssn, size: 11 # exact match
226
+ # attribute :password, size: 8..64 # range
227
+ # attribute :avatar, size 1..(5 * MEGABYTE)
228
+ # end
229
+ #
230
+ # signup = Signup.new(password: 'a-very-long-password')
231
+ # signup.valid? # => true
232
+ #
233
+ # signup = Signup.new(password: 'short')
234
+ # signup.valid? # => false
235
+ def attribute(name, options = {})
236
+ define_attribute(name, options)
237
+ validates(name, options)
238
+ end
239
+
240
+ def defined_attributes
241
+ @defined_attributes ||= Set.new
242
+ end
243
+
244
+ private
245
+
246
+ # @since 0.2.2
247
+ # @api private
248
+ def define_attribute(name, options)
249
+ type = options.delete(:type)
250
+ define_accessor(name, type)
251
+ defined_attributes.add(name.to_s)
252
+
253
+ if options[:confirmation]
254
+ confirmation_accessor = "#{ name }_confirmation"
255
+ define_accessor(confirmation_accessor, type)
256
+ defined_attributes.add(confirmation_accessor)
257
+ end
258
+ end
259
+
260
+ # @since 0.2.2
261
+ # @api private
262
+ def define_accessor(name, type)
263
+ if type
264
+ define_reader(name)
265
+ define_coerced_writer(name, type)
266
+ else
267
+ define_reader(name)
268
+ define_writer(name)
269
+ end
270
+ end
271
+
272
+ # @since 0.2.2
273
+ # @api private
274
+ def define_coerced_writer(name, type)
275
+ define_method("#{ name }=") do |value|
276
+ @attributes.set(name, Lotus::Validations::Coercions.coerce(type, value))
277
+ end
278
+ end
279
+
280
+ # @since 0.2.2
281
+ # @api private
282
+ def define_writer(name)
283
+ define_method("#{ name }=") do |value|
284
+ @attributes.set(name, value)
285
+ end
286
+ end
287
+
288
+ # @since 0.2.2
289
+ # @api private
290
+ def define_reader(name)
291
+ define_method(name) do
292
+ @attributes.get(name)
293
+ end
294
+ end
295
+ end
296
+
297
+ # Create a new instance with the given attributes
298
+ #
299
+ # @param attributes [#to_h] an Hash like object which contains the
300
+ # attributes
301
+ #
302
+ # @since 0.2.2
303
+ #
304
+ # @example Initialize with Hash
305
+ # require 'lotus/validations'
306
+ #
307
+ # class Signup
308
+ # include Lotus::Validations
309
+ #
310
+ # attribute :name
311
+ # end
312
+ #
313
+ # signup = Signup.new(name: 'Luca')
314
+ #
315
+ # @example Initialize with Hash like
316
+ # require 'lotus/validations'
317
+ #
318
+ # class Params
319
+ # def initialize(attributes)
320
+ # @attributes = Hash[*attributes]
321
+ # end
322
+ #
323
+ # def to_h
324
+ # @attributes.to_h
325
+ # end
326
+ # end
327
+ #
328
+ # class Signup
329
+ # include Lotus::Validations
330
+ #
331
+ # attribute :name
332
+ # end
333
+ #
334
+ # params = Params.new([:name, 'Luca'])
335
+ # signup = Signup.new(params)
336
+ #
337
+ # signup.name # => "Luca"
338
+ def initialize(attributes)
339
+ @attributes ||= Utils::Attributes.new
340
+
341
+ attributes.to_h.each do |key, value|
342
+ public_send("#{ key }=", value) if assign_attribute?(key)
343
+ end
344
+ end
345
+
346
+ private
347
+ # @since 0.2.2
348
+ # @api private
349
+ def assign_attribute?(attr)
350
+ self.class.defined_attributes.include?(attr.to_s)
351
+ end
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,45 @@
1
+ module Lotus
2
+ module Validations
3
+ # @since 0.2.2
4
+ # @api private
5
+ class BlankValueChecker
6
+ # @since 0.2.2
7
+ # @api private
8
+ BLANK_STRING_MATCHER = /\A[[:space:]]*\z/.freeze
9
+
10
+ def initialize(value)
11
+ @value = value
12
+ end
13
+
14
+ # Checks if the value is "blank".
15
+ #
16
+ # @since 0.2.2
17
+ # @api private
18
+ def blank_value?
19
+ _nil_value? || _blank_string? || _empty_value?
20
+ end
21
+
22
+ private
23
+
24
+ # Checks if the value is `nil`.
25
+ #
26
+ # @since 0.2.2
27
+ # @api private
28
+ def _nil_value?
29
+ @value.nil?
30
+ end
31
+
32
+ # @since 0.2.2
33
+ # @api private
34
+ def _blank_string?
35
+ (@value.respond_to?(:match) and @value.match(BLANK_STRING_MATCHER))
36
+ end
37
+
38
+ # @since 0.2.2
39
+ # @api private
40
+ def _empty_value?
41
+ (@value.respond_to?(:empty?) and @value.empty?)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -10,7 +10,7 @@ module Lotus
10
10
  # Coerces the given values with the given type
11
11
  #
12
12
  # @param coercer [Class] the type
13
- # @param values [Array] of objects to be coerced
13
+ # @param value [Array] of objects to be coerced
14
14
  # @param blk [Proc] an optional block to pass to the custom coercer
15
15
  #
16
16
  # @return [Object,nil] The result of the coercion, if possible
@@ -19,11 +19,11 @@ module Lotus
19
19
  #
20
20
  # @since 0.1.0
21
21
  # @api private
22
- def self.coerce(coercer, *values, &blk)
22
+ def self.coerce(coercer, value, &blk)
23
23
  if ::Lotus::Utils::Kernel.respond_to?(coercer.to_s)
24
- ::Lotus::Utils::Kernel.__send__(coercer.to_s, *values, &blk) rescue nil
24
+ ::Lotus::Utils::Kernel.__send__(coercer.to_s, value, &blk) rescue nil
25
25
  else
26
- coercer.new(*values, &blk)
26
+ coercer.new(value, &blk)
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,73 @@
1
+ module Lotus
2
+ module Validations
3
+ # A set of validations defined on an object
4
+ #
5
+ # @since 0.2.2
6
+ # @api private
7
+ class ValidationSet
8
+ # Allowed validations
9
+ #
10
+ # @since 0.2.2
11
+ # @api private
12
+ VALIDATIONS = [
13
+ :presence,
14
+ :acceptance,
15
+ :format,
16
+ :inclusion,
17
+ :exclusion,
18
+ :confirmation,
19
+ :size
20
+ ].freeze
21
+
22
+ # @since 0.2.2
23
+ # @api private
24
+ def initialize
25
+ @validations = Hash.new {|h,k| h[k] = {} }
26
+ end
27
+
28
+ # @since 0.2.2
29
+ # @api private
30
+ def add(name, options)
31
+ @validations[name.to_sym].merge!(
32
+ validate_options!(name, options)
33
+ )
34
+ end
35
+
36
+ # @since 0.2.2
37
+ # @api private
38
+ def each(&blk)
39
+ @validations.each(&blk)
40
+ end
41
+
42
+ # @since 0.2.2
43
+ # @api private
44
+ def each_key(&blk)
45
+ @validations.each_key(&blk)
46
+ end
47
+
48
+ private
49
+ # Checks at the loading time if the user defined validations are recognized
50
+ #
51
+ # @param name [Symbol] the attribute name
52
+ # @param options [Hash] the set of validations associated with the given attribute
53
+ #
54
+ # @raise [ArgumentError] if at least one of the validations are not
55
+ # recognized
56
+ #
57
+ # @since 0.2.2
58
+ # @api private
59
+ def validate_options!(name, options)
60
+ if (unknown = (options.keys - VALIDATIONS)) && unknown.any?
61
+ raise ArgumentError.new(%(Unknown validation(s): #{ unknown.join ', ' } for "#{ name }" attribute))
62
+ end
63
+
64
+ # FIXME remove
65
+ if options[:confirmation]
66
+ add(:"#{ name }_confirmation", {})
67
+ end
68
+
69
+ options
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,28 @@
1
+ module Lotus
2
+ module Validations
3
+ # Validate given validations and return a set of errors
4
+ #
5
+ # @since 0.2.2
6
+ # @api private
7
+ class Validator
8
+ def initialize(validation_set, attributes)
9
+ @validation_set = validation_set
10
+ @attributes = attributes
11
+ end
12
+
13
+ # @since 0.2.2
14
+ # @api private
15
+ def validate
16
+ Errors.new.tap do |errors|
17
+ @validation_set.each do |name, validations|
18
+ value = @attributes[name]
19
+ value = @attributes[name.to_s] if value.nil?
20
+
21
+ attribute = Attribute.new(@attributes, name, value, validations)
22
+ errors.add name, *attribute.validate
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,6 +1,6 @@
1
1
  module Lotus
2
2
  module Validations
3
3
  # @since 0.1.0
4
- VERSION = '0.2.1'.freeze
4
+ VERSION = '0.2.2'.freeze
5
5
  end
6
6
  end
@@ -1,7 +1,9 @@
1
1
  require 'lotus/utils/hash'
2
2
  require 'lotus/validations/version'
3
- require 'lotus/validations/attribute_set'
4
- require 'lotus/validations/attributes'
3
+ require 'lotus/validations/blank_value_checker'
4
+ require 'lotus/validations/attribute_definer'
5
+ require 'lotus/validations/validation_set'
6
+ require 'lotus/validations/validator'
5
7
  require 'lotus/validations/attribute'
6
8
  require 'lotus/validations/errors'
7
9
 
@@ -19,16 +21,34 @@ module Lotus
19
21
  #
20
22
  # @see http://www.ruby-doc.org/core/Module.html#method-i-included
21
23
  def self.included(base)
22
- base.extend ClassMethods
24
+ base.class_eval do
25
+ extend ClassMethods
26
+ include AttributeDefiner
27
+ end
23
28
  end
24
29
 
25
30
  # Validations DSL
26
31
  #
27
32
  # @since 0.1.0
28
33
  module ClassMethods
34
+ # Override Ruby's hook for class inheritance. When a class includes
35
+ # Lotus::Validations and it is subclassed, this passes
36
+ # the attributes from the superclass to the subclass.
37
+ #
38
+ # @param base [Class] the target action
39
+ #
40
+ # @since 0.2.2
41
+ # @api private
42
+ #
43
+ # @see http://www.ruby-doc.org/core/Class.html#method-i-inherited
44
+ def inherited(base)
45
+ transfer_validations_to_base(base)
46
+ super
47
+ end
48
+
29
49
  # Override Ruby's hook for modules. When a module includes
30
50
  # Lotus::Validations and it is included in a class or module, this passes
31
- # the attributes from the module to the base.
51
+ # the validations from the module to the base.
32
52
  #
33
53
  # @param base [Class] the target action
34
54
  #
@@ -60,167 +80,31 @@ module Lotus
60
80
  include Lotus::Validations
61
81
  end
62
82
 
63
- attributes.each do |attribute, options|
64
- base.attribute attribute, options
65
- end
83
+ super
84
+
85
+ transfer_validations_to_base(base)
66
86
  end
67
87
 
68
- # Define an attribute
88
+ # Define a validation for an existing attribute
69
89
  #
70
90
  # @param name [#to_sym] the name of the attribute
71
- # @param options [Hash] optional set of validations
72
- # @option options [Class] :type the Ruby type used to coerce the value
73
- # @option options [TrueClass,FalseClass] :acceptance requires Ruby
74
- # thruthiness of the value
75
- # @option options [TrueClass,FalseClass] :confirmation requires the value
76
- # to be confirmed twice
77
- # @option options [#include?] :exclusion requires the value NOT be
78
- # included in the given collection
79
- # @option options [Regexp] :format requires value to match the given
80
- # Regexp
81
- # @option options [#include?] :inclusion requires the value BE included in
82
- # the given collection
83
- # @option options [TrueClass,FalseClass] :presence requires the value be
84
- # included in the given collection
85
- # @option options [Numeric,Range] :size requires value's to be equal or
86
- # included by the given validator
87
- #
88
- # @raise [ArgumentError] if an unknown or mispelled validation is given
89
- #
90
- # @example Attributes
91
- # require 'lotus/validations'
92
- #
93
- # class Person
94
- # include Lotus::Validations
95
- #
96
- # attribute :name
97
- # end
98
- #
99
- # person = Person.new(name: 'Luca', age: 32)
100
- # person.name # => "Luca"
101
- # person.age # => raises NoMethodError because `:age` wasn't defined as attribute.
102
- #
103
- # @example Standard coercions
104
- # require 'lotus/validations'
105
- #
106
- # class Person
107
- # include Lotus::Validations
108
- #
109
- # attribute :fav_number, type: Integer
110
- # end
111
- #
112
- # person = Person.new(fav_number: '23')
113
- # person.valid?
114
- #
115
- # person.fav_number # => 23
116
- #
117
- # @example Custom coercions
118
- # require 'lotus/validations'
119
- #
120
- # class FavNumber
121
- # def initialize(number)
122
- # @number = number
123
- # end
124
- # end
125
- #
126
- # class BirthDate
127
- # end
128
- #
129
- # class Person
130
- # include Lotus::Validations
131
- #
132
- # attribute :fav_number, type: FavNumber
133
- # attribute :date, type: BirthDate
134
- # end
91
+ # @param options [Hash] set of validations
135
92
  #
136
- # person = Person.new(fav_number: '23', date: 'Oct 23, 2014')
137
- # person.valid?
93
+ # @see Lotus::Validations::ClassMethods#validations
138
94
  #
139
- # person.fav_number # => 23
140
- # person.date # => this raises an error, because BirthDate#initialize doesn't accept any arg
141
- #
142
- # @example Acceptance
143
- # require 'lotus/validations'
144
- #
145
- # class Signup
146
- # include Lotus::Validations
147
- #
148
- # attribute :terms_of_service, acceptance: true
149
- # end
150
- #
151
- # signup = Signup.new(terms_of_service: '1')
152
- # signup.valid? # => true
153
- #
154
- # signup = Signup.new(terms_of_service: '')
155
- # signup.valid? # => false
156
- #
157
- # @example Confirmation
158
- # require 'lotus/validations'
159
- #
160
- # class Signup
161
- # include Lotus::Validations
162
- #
163
- # attribute :password, confirmation: true
164
- # end
165
- #
166
- # signup = Signup.new(password: 'secret', password_confirmation: 'secret')
167
- # signup.valid? # => true
168
- #
169
- # signup = Signup.new(password: 'secret', password_confirmation: 'x')
170
- # signup.valid? # => false
171
- #
172
- # @example Exclusion
173
- # require 'lotus/validations'
174
- #
175
- # class Signup
176
- # include Lotus::Validations
177
- #
178
- # attribute :music, exclusion: ['pop']
179
- # end
180
- #
181
- # signup = Signup.new(music: 'rock')
182
- # signup.valid? # => true
183
- #
184
- # signup = Signup.new(music: 'pop')
185
- # signup.valid? # => false
186
- #
187
- # @example Format
188
- # require 'lotus/validations'
189
- #
190
- # class Signup
191
- # include Lotus::Validations
192
- #
193
- # attribute :name, format: /\A[a-zA-Z]+\z/
194
- # end
195
- #
196
- # signup = Signup.new(name: 'Luca')
197
- # signup.valid? # => true
198
- #
199
- # signup = Signup.new(name: '23')
200
- # signup.valid? # => false
201
- #
202
- # @example Inclusion
95
+ # @example Presence
203
96
  # require 'lotus/validations'
204
97
  #
205
98
  # class Signup
206
99
  # include Lotus::Validations
207
100
  #
208
- # attribute :age, inclusion: 18..99
209
- # end
210
- #
211
- # signup = Signup.new(age: 32)
212
- # signup.valid? # => true
213
- #
214
- # signup = Signup.new(age: 17)
215
- # signup.valid? # => false
101
+ # def initialize(attributes = {})
102
+ # @name = attributes.fetch(:name)
103
+ # end
216
104
  #
217
- # @example Presence
218
- # require 'lotus/validations'
105
+ # attr_accessor :name
219
106
  #
220
- # class Signup
221
- # include Lotus::Validations
222
- #
223
- # attribute :name, presence: true
107
+ # validates :name, presence: true
224
108
  # end
225
109
  #
226
110
  # signup = Signup.new(name: 'Luca')
@@ -228,43 +112,32 @@ module Lotus
228
112
  #
229
113
  # signup = Signup.new(name: nil)
230
114
  # signup.valid? # => false
115
+ def validates(name, options)
116
+ validations.add(name, options)
117
+ end
118
+
119
+ # Set of user defined validations
231
120
  #
232
- # @example Size
233
- # require 'lotus/validations'
234
- #
235
- # class Signup
236
- # MEGABYTE = 1024 ** 2
237
- # include Lotus::Validations
238
- #
239
- # attribute :ssn, size: 11 # exact match
240
- # attribute :password, size: 8..64 # range
241
- # attribute :avatar, size 1..(5 * MEGABYTE)
242
- # end
243
- #
244
- # signup = Signup.new(password: 'a-very-long-password')
245
- # signup.valid? # => true
121
+ # @return [Hash]
246
122
  #
247
- # signup = Signup.new(password: 'short')
248
- # signup.valid? # => false
249
- def attribute(name, options = {})
250
- attributes.add(name, options)
251
-
252
- class_eval %{
253
- def #{ name }
254
- @attributes.get(:#{ name })
255
- end
256
- }
123
+ # @since 0.2.2
124
+ # @api private
125
+ def validations
126
+ @validations ||= ValidationSet.new
257
127
  end
258
128
 
259
129
  private
260
- # Set of user defined attributes
130
+
131
+ # Transfers attributes to a base class
261
132
  #
262
- # @return [Hash]
133
+ # @param base [Module] the base class to transfer attributes to
263
134
  #
264
- # @since 0.1.0
135
+ # @since 0.2.2
265
136
  # @api private
266
- def attributes
267
- @attributes ||= AttributeSet.new
137
+ def transfer_validations_to_base(base)
138
+ validations.each do |attribute, options|
139
+ base.validates attribute, options
140
+ end
268
141
  end
269
142
  end
270
143
 
@@ -315,52 +188,8 @@ module Lotus
315
188
  # # #<Lotus::Validations::Error:0x007fe00cee30d8 @attribute=:age, @validation=:size, @expected=18..99, @actual=17>
316
189
  # # ]
317
190
  # # }>
318
- attr_reader :errors
319
-
320
- # Create a new instance with the given attributes
321
- #
322
- # @param attributes [#to_h] an Hash like object which contains the
323
- # attributes
324
- #
325
- # @since 0.1.0
326
- #
327
- # @example Initialize with Hash
328
- # require 'lotus/validations'
329
- #
330
- # class Signup
331
- # include Lotus::Validations
332
- #
333
- # attribute :name
334
- # end
335
- #
336
- # signup = Signup.new(name: 'Luca')
337
- #
338
- # @example Initialize with Hash like
339
- # require 'lotus/validations'
340
- #
341
- # class Params
342
- # def initialize(attributes)
343
- # @attributes = Hash[*attributes]
344
- # end
345
- #
346
- # def to_h
347
- # @attributes.to_h
348
- # end
349
- # end
350
- #
351
- # class Signup
352
- # include Lotus::Validations
353
- #
354
- # attribute :name
355
- # end
356
- #
357
- # params = Params.new([:name, 'Luca'])
358
- # signup = Signup.new(params)
359
- #
360
- # signup.name # => "Luca"
361
- def initialize(attributes)
362
- @attributes = Attributes.new(defined_attributes, attributes)
363
- @errors = Errors.new
191
+ def errors
192
+ @errors ||= Errors.new
364
193
  end
365
194
 
366
195
  # Checks if the current data satisfies the defined validations
@@ -369,13 +198,10 @@ module Lotus
369
198
  #
370
199
  # @since 0.1.0
371
200
  def valid?
372
- @errors.clear
201
+ validator = Validator.new(defined_validations, read_attributes)
202
+ @errors = validator.validate
373
203
 
374
- @attributes.each do |name, attribute|
375
- @errors.add(name, *attribute.validate)
376
- end
377
-
378
- @errors.empty?
204
+ errors.empty?
379
205
  end
380
206
 
381
207
  # Iterates thru the defined attributes and their values
@@ -396,18 +222,30 @@ module Lotus
396
222
  #
397
223
  # @since 0.1.0
398
224
  def to_h
399
- @attributes.dup
225
+ Utils::Hash.new(read_attributes).deep_dup
400
226
  end
401
227
 
402
228
  private
403
- # The set of user defined attributes.
229
+ # The set of user defined validations.
404
230
  #
405
- # @since 0.1.0
231
+ # @since 0.2.2
406
232
  # @api private
407
233
  #
408
- # @see Lotus::Validations::ClassMethods#attributes
409
- def defined_attributes
410
- self.class.__send__(:attributes)
234
+ # @see Lotus::Validations::ClassMethods#validations
235
+ def defined_validations
236
+ self.class.__send__(:validations)
237
+ end
238
+
239
+ # Builds a Hash of current attribute values.
240
+ #
241
+ # @since 0.2.2
242
+ # @api private
243
+ def read_attributes
244
+ {}.tap do |attributes|
245
+ defined_validations.each_key do |attribute|
246
+ attributes[attribute] = public_send(attribute)
247
+ end
248
+ end
411
249
  end
412
250
  end
413
251
  end
@@ -6,8 +6,8 @@ require 'lotus/validations/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'lotus-validations'
8
8
  spec.version = Lotus::Validations::VERSION
9
- spec.authors = ['Luca Guidi']
10
- spec.email = ['me@lucaguidi.com']
9
+ spec.authors = ['Luca Guidi', 'Trung Lê']
10
+ spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com']
11
11
  spec.summary = %q{Validations mixin for Ruby objects}
12
12
  spec.description = %q{Validations mixin for Ruby objects and support for Lotus}
13
13
  spec.homepage = 'http://lotusrb.org'
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ['lib']
20
20
  spec.required_ruby_version = '>= 2.0.0'
21
21
 
22
- spec.add_dependency 'lotus-utils', '~> 0.3', '>= 0.3.2'
22
+ spec.add_dependency 'lotus-utils', '~> 0.3', '>= 0.3.3'
23
23
 
24
24
  spec.add_development_dependency 'bundler', '~> 1.6'
25
25
  spec.add_development_dependency 'minitest', '~> 5'
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lotus-validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
+ - Trung Lê
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2014-12-23 00:00:00.000000000 Z
12
+ date: 2015-01-08 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: lotus-utils
@@ -19,7 +20,7 @@ dependencies:
19
20
  version: '0.3'
20
21
  - - ">="
21
22
  - !ruby/object:Gem::Version
22
- version: 0.3.2
23
+ version: 0.3.3
23
24
  type: :runtime
24
25
  prerelease: false
25
26
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +30,7 @@ dependencies:
29
30
  version: '0.3'
30
31
  - - ">="
31
32
  - !ruby/object:Gem::Version
32
- version: 0.3.2
33
+ version: 0.3.3
33
34
  - !ruby/object:Gem::Dependency
34
35
  name: bundler
35
36
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +76,7 @@ dependencies:
75
76
  description: Validations mixin for Ruby objects and support for Lotus
76
77
  email:
77
78
  - me@lucaguidi.com
79
+ - trung.le@ruby-journal.com
78
80
  executables: []
79
81
  extensions: []
80
82
  extra_rdoc_files: []
@@ -85,11 +87,13 @@ files:
85
87
  - lib/lotus-validations.rb
86
88
  - lib/lotus/validations.rb
87
89
  - lib/lotus/validations/attribute.rb
88
- - lib/lotus/validations/attribute_set.rb
89
- - lib/lotus/validations/attributes.rb
90
+ - lib/lotus/validations/attribute_definer.rb
91
+ - lib/lotus/validations/blank_value_checker.rb
90
92
  - lib/lotus/validations/coercions.rb
91
93
  - lib/lotus/validations/error.rb
92
94
  - lib/lotus/validations/errors.rb
95
+ - lib/lotus/validations/validation_set.rb
96
+ - lib/lotus/validations/validator.rb
93
97
  - lib/lotus/validations/version.rb
94
98
  - lotus-validations.gemspec
95
99
  homepage: http://lotusrb.org
@@ -112,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
116
  version: '0'
113
117
  requirements: []
114
118
  rubyforge_project:
115
- rubygems_version: 2.2.2
119
+ rubygems_version: 2.4.5
116
120
  signing_key:
117
121
  specification_version: 4
118
122
  summary: Validations mixin for Ruby objects
@@ -1,51 +0,0 @@
1
- class AttributeSet
2
- VALIDATIONS = [:presence, :acceptance, :format, :inclusion, :exclusion, :confirmation, :size, :type].freeze
3
-
4
- def initialize
5
- @attributes = Hash.new {|h,k| h[k] = {} }
6
- end
7
-
8
- def add(name, options)
9
- @attributes[name.to_sym].merge!(
10
- validate_options!(name, options)
11
- )
12
- end
13
-
14
- def each(&blk)
15
- @attributes.each(&blk)
16
- end
17
-
18
- def iterate(attributes, &blk)
19
- if @attributes.any?
20
- @attributes.each(&blk)
21
- else
22
- attributes.each do |name, _|
23
- blk.call(name, {})
24
- end
25
- end
26
- end
27
-
28
- private
29
- # Checks at the loading time if the user defined validations are recognized
30
- #
31
- # @param name [Symbol] the attribute name
32
- # @param options [Hash] the set of validations associated with the given attribute
33
- #
34
- # @raise [ArgumentError] if at least one of the validations are not
35
- # recognized
36
- #
37
- # @since 0.2.0
38
- # @api private
39
- def validate_options!(name, options)
40
- if (unknown = (options.keys - VALIDATIONS)) && unknown.any?
41
- raise ArgumentError.new(%(Unknown validation(s): #{ unknown.join ', ' } for "#{ name }" attribute))
42
- end
43
-
44
- # FIXME remove
45
- if options[:confirmation]
46
- add(:"#{ name }_confirmation", {})
47
- end
48
-
49
- options
50
- end
51
- end
@@ -1,38 +0,0 @@
1
- module Lotus
2
- module Validations
3
- class Attributes
4
- def initialize(definitions, attributes)
5
- attributes = attributes.to_h
6
-
7
- @attributes = Utils::Hash.new.tap do |result|
8
- definitions.iterate(attributes) do |name, validations|
9
- value = attributes[name]
10
- value = attributes[name.to_s] if value.nil?
11
-
12
- result[name] = Attribute.new(attributes, name, value, validations)
13
- end
14
- end
15
- end
16
-
17
- def get(name)
18
- (attr = @attributes[name]) and attr.value
19
- end
20
-
21
- def dup
22
- Utils::Hash.new(to_h).deep_dup
23
- end
24
-
25
- def each(&blk)
26
- @attributes.each(&blk)
27
- end
28
-
29
- def to_h
30
- ::Hash.new.tap do |result|
31
- each do |name, _|
32
- result[name] = get(name)
33
- end
34
- end
35
- end
36
- end
37
- end
38
- end