poncho 0.0.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.
@@ -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