poncho 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ module Poncho
2
+ module Returns
3
+ class InvalidReturn < ServerError
4
+ end
5
+
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def returns(*resources)
12
+ @returns = resources if resources.any?
13
+ @returns ||= []
14
+ end
15
+ end
16
+
17
+ def body(value = nil)
18
+ if value && success? && self.class.returns.none? {|res| value.is_a?(res) }
19
+ raise InvalidReturn, "Invalid body: #{value}"
20
+ end
21
+
22
+ super
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,198 @@
1
+ module Poncho
2
+ # == Poncho Validations
3
+ #
4
+ # Provides a full validation framework to your objects.
5
+ #
6
+ # A minimal implementation could be:
7
+ #
8
+ # class Person
9
+ # include Poncho::Validations
10
+ #
11
+ # attr_accessor :first_name, :last_name
12
+ #
13
+ # validates_each :first_name, :last_name do |record, attr, value|
14
+ # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
15
+ # end
16
+ # end
17
+ #
18
+ # Which provides you with the full standard validation stack that you
19
+ # know from Active Record:
20
+ #
21
+ # person = Person.new
22
+ # person.valid? # => true
23
+ # person.invalid? # => false
24
+ #
25
+ # person.first_name = 'zoolander'
26
+ # person.valid? # => false
27
+ # person.invalid? # => true
28
+ # person.errors # => #<OrderedHash {:first_name=>["starts with z."]}>
29
+ #
30
+ # Note that <tt>Poncho::Validations</tt> automatically adds an +errors+ method
31
+ # to your instances initialized with a new <tt>Poncho::Errors</tt> object, so
32
+ # there is no need for you to do this manually.
33
+ #
34
+ module Validations
35
+ def self.included(base)
36
+ base.extend ClassMethods
37
+ base.extend HelperMethods
38
+ end
39
+
40
+ module HelperMethods
41
+ end
42
+
43
+ module ClassMethods
44
+
45
+ VALIDATES_DEFAULT_KEYS = [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
46
+
47
+ # Validates each attribute against a block.
48
+ #
49
+ # class Person
50
+ # include Poncho::Validations
51
+ #
52
+ # attr_accessor :first_name, :last_name
53
+ #
54
+ # validates_each :first_name, :last_name do |record, attr, value|
55
+ # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
56
+ # end
57
+ # end
58
+ #
59
+ # Options:
60
+ # * <tt>:on</tt> - Specifies the context where this validation is active
61
+ # (e.g. <tt>:on => :create</tt> or <tt>:on => :custom_validation_context</tt>)
62
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
63
+ # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
64
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
65
+ # if the validation should occur (e.g. <tt>:if => :allow_validation</tt>,
66
+ # or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method,
67
+ # proc or string should return or evaluate to a true or false value.
68
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
69
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or
70
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
71
+ # method, proc or string should return or evaluate to a true or false value.
72
+ def validates_each(*attr_names, &block)
73
+ options = attr_names.last.is_a?(::Hash) ? attr_names.pop : {}
74
+ validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block
75
+ end
76
+
77
+ # Adds a validation method or block to the class. This is useful when
78
+ # overriding the +validate+ instance method becomes too unwieldy and
79
+ # you're looking for more descriptive declaration of your validations.
80
+ #
81
+ # This can be done with a symbol pointing to a method:
82
+ #
83
+ # class Comment
84
+ # include Poncho::Validations
85
+ #
86
+ # validate :must_be_friends
87
+ #
88
+ # def must_be_friends
89
+ # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee)
90
+ # end
91
+ # end
92
+ #
93
+ # With a block which is passed with the current record to be validated:
94
+ #
95
+ # class Comment
96
+ # include Poncho::Validations
97
+ #
98
+ # validate do |comment|
99
+ # comment.must_be_friends
100
+ # end
101
+ #
102
+ # def must_be_friends
103
+ # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee)
104
+ # end
105
+ # end
106
+ #
107
+ # Or with a block where self points to the current record to be validated:
108
+ #
109
+ # class Comment
110
+ # include Poncho::Validations
111
+ #
112
+ # validate do
113
+ # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee)
114
+ # end
115
+ # end
116
+ #
117
+ def validate(proc = nil, &block)
118
+ proc ||= block
119
+ proc = method(proc) if proc.is_a?(Symbol)
120
+
121
+ validators << proc
122
+ end
123
+
124
+ # List all validators that are being used to validate the model using
125
+ # +validates_with+ method.
126
+ def validators
127
+ @validators ||= []
128
+ end
129
+
130
+ def validates_with(*args, &block)
131
+ options = args.last.is_a?(::Hash) ? args.pop : {}
132
+
133
+ args.each do |klass|
134
+ validator = klass.new(options, &block)
135
+ validate(validator.method(:validate))
136
+ end
137
+ end
138
+
139
+ def validates(*attributes)
140
+ options = attributes.last.is_a?(::Hash) ? attributes.pop : {}
141
+
142
+ validations = options.reject {|key, value| VALIDATES_DEFAULT_KEYS.include?(key) || !value }
143
+ options = options.merge(:attributes => attributes)
144
+
145
+ validations.each do |key, validator_options|
146
+ validator_options = {} if validator_options == true
147
+ validates_with(validator_for_kind(key), validator_options.merge(:attributes => attributes))
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ def validator_for_kind(kind)
154
+ return type if type.is_a?(Class)
155
+ name = kind.to_s.split('_').map {|w| w.capitalize }.join
156
+ const_get("#{name}Validator")
157
+ rescue NameError
158
+ raise ArgumentError, "Unknown validator: #{kind}"
159
+ end
160
+ end
161
+
162
+ # Returns the +Errors+ object that holds all information about attribute error messages.
163
+ def errors
164
+ @errors ||= Errors.new(self)
165
+ end
166
+
167
+ # Runs all the specified validations and returns true if no errors were added
168
+ # otherwise false. Context can optionally be supplied to define which callbacks
169
+ # to test against.
170
+ def valid?(context = nil)
171
+ errors.clear
172
+ run_validations!
173
+ end
174
+
175
+ # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added,
176
+ # false otherwise.
177
+ def invalid?
178
+ !valid?
179
+ end
180
+
181
+ alias :read_attribute_for_validation :send
182
+
183
+ protected
184
+
185
+ def run_validations!
186
+ self.class.validators.each do |validator|
187
+ instance_eval(&validator)
188
+ end
189
+
190
+ errors.empty?
191
+ end
192
+ end
193
+ end
194
+
195
+ Dir[File.dirname(__FILE__) + '/validations/*.rb'].sort.each do |path|
196
+ filename = File.basename(path)
197
+ require "poncho/validations/#{filename}"
198
+ end
@@ -0,0 +1,77 @@
1
+ module Poncho
2
+ module Validations
3
+ class ExclusionValidator < EachValidator
4
+ ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " <<
5
+ "and must be supplied as the :in (or :within) option of the configuration hash"
6
+
7
+ def check_validity!
8
+ unless [:include?, :call].any? { |method| delimiter.respond_to?(method) }
9
+ raise ArgumentError, ERROR_MESSAGE
10
+ end
11
+ end
12
+
13
+ def validate_each(record, attribute, value)
14
+ exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
15
+ if exclusions.send(inclusion_method(exclusions), value)
16
+ record.errors.add(attribute, :exclusion, options.merge(:value => value))
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def delimiter
23
+ @delimiter ||= options[:in] || options[:within]
24
+ end
25
+
26
+ # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible
27
+ # values in the range for equality, so it may be slow for large ranges. The new
28
+ # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the
29
+ # range endpoints.
30
+ def inclusion_method(enumerable)
31
+ enumerable.is_a?(Range) ? :cover? : :include?
32
+ end
33
+ end
34
+
35
+ module HelperMethods
36
+ # Validates that the value of the specified attribute is not in a particular
37
+ # enumerable object.
38
+ #
39
+ # class Person < ActiveRecord::Base
40
+ # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
41
+ # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
42
+ # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed"
43
+ # validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] },
44
+ # :message => "should not be the same as your username or first name"
45
+ # end
46
+ #
47
+ # Configuration options:
48
+ # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be
49
+ # part of. This can be supplied as a proc or lambda which returns an enumerable.
50
+ # If the enumerable is a range the test is performed with <tt>Range#cover?</tt>
51
+ # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>.
52
+ # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
53
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved").
54
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute
55
+ # is +nil+ (default is +false+).
56
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the
57
+ # attribute is blank (default is +false+).
58
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
59
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
60
+ # and <tt>:update</tt>.
61
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the
62
+ # validation should occur (e.g. <tt>:if => :allow_validation</tt>, or
63
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
64
+ # or string should return or evaluate to a true or false value.
65
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if
66
+ # the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>,
67
+ # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
68
+ # proc or string should return or evaluate to a true or false value.
69
+ # * <tt>:strict</tt> - Specifies whether validation should be strict.
70
+ # See <tt>Poncho::Validation#validates!</tt> for more information.
71
+ def validates_exclusion_of(*attr_names)
72
+ options = attr_names.last.is_a?(::Hash) ? attr_names.pop : {}
73
+ validates_with ExclusionValidator, options.merge(:attributes => attr_names)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,105 @@
1
+ module Poncho
2
+ module Validations
3
+ class FormatValidator < EachValidator
4
+ def validate_each(record, attribute, value)
5
+ if options[:with]
6
+ regexp = option_call(record, :with)
7
+ record_error(record, attribute, :with, value) if value.to_s !~ regexp
8
+ elsif options[:without]
9
+ regexp = option_call(record, :without)
10
+ record_error(record, attribute, :without, value) if value.to_s =~ regexp
11
+ end
12
+ end
13
+
14
+ def check_validity!
15
+ unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
16
+ raise ArgumentError, "Either :with or :without must be supplied (but not both)"
17
+ end
18
+
19
+ check_options_validity(options, :with)
20
+ check_options_validity(options, :without)
21
+ end
22
+
23
+ private
24
+
25
+ def option_call(record, name)
26
+ option = options[name]
27
+ option.respond_to?(:call) ? option.call(record) : option
28
+ end
29
+
30
+ def record_error(record, attribute, name, value)
31
+ record.errors.add(attribute, options.merge(:value => value))
32
+ end
33
+
34
+ def check_options_validity(options, name)
35
+ option = options[name]
36
+ if option && !option.is_a?(Regexp) && !option.respond_to?(:call)
37
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
38
+ end
39
+ end
40
+ end
41
+
42
+ module HelperMethods
43
+ # Validates whether the value of the specified attribute is of the correct form,
44
+ # going by the regular expression provided. You can require that the attribute
45
+ # matches the regular expression:
46
+ #
47
+ # class Person < ActiveRecord::Base
48
+ # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
49
+ # end
50
+ #
51
+ # Alternatively, you can require that the specified attribute does _not_ match
52
+ # the regular expression:
53
+ #
54
+ # class Person < ActiveRecord::Base
55
+ # validates_format_of :email, :without => /NOSPAM/
56
+ # end
57
+ #
58
+ # You can also provide a proc or lambda which will determine the regular
59
+ # expression that will be used to validate the attribute.
60
+ #
61
+ # class Person < ActiveRecord::Base
62
+ # # Admin can have number as a first letter in their screen name
63
+ # validates_format_of :screen_name,
64
+ # :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i }
65
+ # end
66
+ #
67
+ # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string,
68
+ # <tt>^</tt> and <tt>$</tt> match the start/end of a line.
69
+ #
70
+ # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In
71
+ # addition, both must be a regular expression or a proc or lambda, or else an
72
+ # exception will be raised.
73
+ #
74
+ # Configuration options:
75
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid").
76
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute
77
+ # is +nil+ (default is +false+).
78
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the
79
+ # attribute is blank (default is +false+).
80
+ # * <tt>:with</tt> - Regular expression that if the attribute matches will
81
+ # result in a successful validation. This can be provided as a proc or lambda
82
+ # returning regular expression which will be called at runtime.
83
+ # * <tt>:without</tt> - Regular expression that if the attribute does not match
84
+ # will result in a successful validation. This can be provided as a proc or
85
+ # lambda returning regular expression which will be called at runtime.
86
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
87
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
88
+ # and <tt>:update</tt>.
89
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the
90
+ # validation should occur (e.g. <tt>:if => :allow_validation</tt>, or
91
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
92
+ # or string should return or evaluate to a true or false value.
93
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if
94
+ # the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>,
95
+ # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
96
+ # proc or string should return or evaluate to a true or false value.
97
+ # * <tt>:strict</tt> - Specifies whether validation should be strict.
98
+ # See <tt>Poncho::Validation#validates!</tt> for more information.
99
+ def validates_format_of(*attr_names)
100
+ options = attr_names.last.is_a?(::Hash) ? attr_names.pop : {}
101
+ validates_with FormatValidator, options.merge(:attributes => attr_names)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,77 @@
1
+ module Poncho
2
+ module Validations
3
+ class InclusionValidator < EachValidator
4
+ ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " <<
5
+ "and must be supplied as the :in (or :within) option of the configuration hash"
6
+
7
+ def check_validity!
8
+ unless [:include?, :call].any?{ |method| delimiter.respond_to?(method) }
9
+ raise ArgumentError, ERROR_MESSAGE
10
+ end
11
+ end
12
+
13
+ def validate_each(record, attribute, value)
14
+ exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
15
+ unless exclusions.send(inclusion_method(exclusions), value)
16
+ record.errors.add(attribute, :inclusion, options.merge(:value => value))
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def delimiter
23
+ @delimiter ||= options[:in] || options[:within]
24
+ end
25
+
26
+ # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible
27
+ # values in the range for equality, so it may be slow for large ranges. The new
28
+ # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the
29
+ # range endpoints.
30
+ def inclusion_method(enumerable)
31
+ enumerable.is_a?(Range) ? :cover? : :include?
32
+ end
33
+ end
34
+
35
+ module HelperMethods
36
+ # Validates whether the value of the specified attribute is available in a
37
+ # particular enumerable object.
38
+ #
39
+ # class Person < ActiveRecord::Base
40
+ # validates_inclusion_of :gender, :in => %w( m f )
41
+ # validates_inclusion_of :age, :in => 0..99
42
+ # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %{value} is not included in the list"
43
+ # validates_inclusion_of :states, :in => lambda{ |person| STATES[person.country] }
44
+ # end
45
+ #
46
+ # Configuration options:
47
+ # * <tt>:in</tt> - An enumerable object of available items. This can be
48
+ # supplied as a proc or lambda which returns an enumerable. If the enumerable
49
+ # is a range the test is performed with <tt>Range#cover?</tt>
50
+ # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>.
51
+ # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
52
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "is not
53
+ # included in the list").
54
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute
55
+ # is +nil+ (default is +false+).
56
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the
57
+ # attribute is blank (default is +false+).
58
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
59
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
60
+ # and <tt>:update</tt>.
61
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
62
+ # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or
63
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
64
+ # or string should return or evaluate to a true or false value.
65
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
66
+ # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>,
67
+ # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
68
+ # proc or string should return or evaluate to a true or false value.
69
+ # * <tt>:strict</tt> - Specifies whether validation should be strict.
70
+ # See <tt>Poncho::Validation#validates!</tt> for more information.
71
+ def validates_inclusion_of(*attr_names)
72
+ options = attr_names.last.is_a?(::Hash) ? attr_names.pop : {}
73
+ validates_with InclusionValidator, options.merge(:attributes => attr_names)
74
+ end
75
+ end
76
+ end
77
+ end