poncho 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,123 @@
1
+ module Poncho
2
+ module Validations
3
+ class LengthValidator < EachValidator
4
+ MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze
5
+ CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze
6
+
7
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
8
+
9
+ def initialize(options)
10
+ if range = (options.delete(:in) || options.delete(:within))
11
+ raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
12
+ options[:minimum], options[:maximum] = range.begin, range.end
13
+ options[:maximum] -= 1 if range.exclude_end?
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ def check_validity!
20
+ keys = CHECKS.keys & options.keys
21
+
22
+ if keys.empty?
23
+ raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.'
24
+ end
25
+
26
+ keys.each do |key|
27
+ value = options[key]
28
+
29
+ unless value.is_a?(Integer) && value >= 0
30
+ raise ArgumentError, ":#{key} must be a nonnegative Integer"
31
+ end
32
+ end
33
+ end
34
+
35
+ def validate_each(record, attribute, value)
36
+ value = tokenize(value)
37
+ value_length = value.respond_to?(:length) ? value.length : value.to_s.length
38
+
39
+ CHECKS.each do |key, validity_check|
40
+ next unless check_value = options[key]
41
+ next if value_length.send(validity_check, check_value)
42
+
43
+ errors_options = options.dup
44
+ errors_options[:count] = check_value
45
+
46
+ default_message = options[MESSAGES[key]]
47
+ errors_options[:message] ||= default_message if default_message
48
+
49
+ record.errors.add(attribute, MESSAGES[key], errors_options)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def tokenize(value)
56
+ if value.kind_of?(String) && options[:tokenizer]
57
+ return options[:tokenizer].call(value)
58
+ end
59
+
60
+ value
61
+ end
62
+ end
63
+
64
+ module HelperMethods
65
+ # Validates that the specified attribute matches the length restrictions supplied.
66
+ # Only one option can be used at a time:
67
+ #
68
+ # class Person < ActiveRecord::Base
69
+ # validates_length_of :first_name, :maximum => 30
70
+ # validates_length_of :last_name, :maximum => 30, :message => "less than 30 if you don't mind"
71
+ # validates_length_of :fax, :in => 7..32, :allow_nil => true
72
+ # validates_length_of :phone, :in => 7..32, :allow_blank => true
73
+ # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
74
+ # validates_length_of :zip_code, :minimum => 5, :too_short => "please enter at least 5 characters"
75
+ # validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with 4 characters... don't play me."
76
+ # validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least 100 words.",
77
+ # :tokenizer => lambda { |str| str.scan(/\w+/) }
78
+ # end
79
+ #
80
+ # Configuration options:
81
+ # * <tt>:minimum</tt> - The minimum size of the attribute.
82
+ # * <tt>:maximum</tt> - The maximum size of the attribute.
83
+ # * <tt>:is</tt> - The exact size of the attribute.
84
+ # * <tt>:within</tt> - A range specifying the minimum and maximum size of the
85
+ # attribute.
86
+ # * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
87
+ # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
88
+ # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
89
+ # * <tt>:too_long</tt> - The error message if the attribute goes over the
90
+ # maximum (default is: "is too long (maximum is %{count} characters)").
91
+ # * <tt>:too_short</tt> - The error message if the attribute goes under the
92
+ # minimum (default is: "is too short (min is %{count} characters)").
93
+ # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method
94
+ # and the attribute is the wrong size (default is: "is the wrong length
95
+ # should be %{count} characters)").
96
+ # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>,
97
+ # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate
98
+ # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
99
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
100
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
101
+ # and <tt>:update</tt>.
102
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
103
+ # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or
104
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
105
+ # or string should return or evaluate to a true or false value.
106
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
107
+ # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>,
108
+ # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
109
+ # method, proc or string should return or evaluate to a true or false value.
110
+ # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string.
111
+ # (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to count words
112
+ # as in above example). Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
113
+ # * <tt>:strict</tt> - Specifies whether validation should be strict.
114
+ # See <tt>Poncho::Validation#validates!</tt> for more information.
115
+ def validates_length_of(*attr_names)
116
+ options = attr_names.last.is_a?(::Hash) ? attr_names.pop : {}
117
+ validates_with LengthValidator, options.merge(:attributes => attr_names)
118
+ end
119
+
120
+ alias_method :validates_size_of, :validates_length_of
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,49 @@
1
+ module Poncho
2
+ module Validations
3
+ class PresenceValidator < EachValidator
4
+ def validate_each(record, attribute, value)
5
+ if !value || value == ""
6
+ record.errors.add(attribute, :presence, options.merge(:value => value))
7
+ end
8
+ end
9
+ end
10
+
11
+ module HelperMethods
12
+ # Validates that the specified attributes are not blank (as defined by
13
+ # Object#blank?). Happens by default on save. Example:
14
+ #
15
+ # class Person < ActiveRecord::Base
16
+ # validates_presence_of :first_name
17
+ # end
18
+ #
19
+ # The first_name attribute must be in the object and it cannot be blank.
20
+ #
21
+ # If you want to validate the presence of a boolean field (where the real values
22
+ # are true and false), you will want to use <tt>validates_inclusion_of :field_name,
23
+ # :in => [true, false]</tt>.
24
+ #
25
+ # This is due to the way Object#blank? handles boolean values:
26
+ # <tt>false.blank? # => true</tt>.
27
+ #
28
+ # Configuration options:
29
+ # * <tt>:message</tt> - A custom error message (default is: "can't be blank").
30
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
31
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
32
+ # and <tt>:update</tt>.
33
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
34
+ # the validation should occur (e.g. <tt>:if => :allow_validation</tt>, or
35
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
36
+ # or string should return or evaluate to a true or false value.
37
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
38
+ # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>,
39
+ # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
40
+ # proc or string should return or evaluate to a true or false value.
41
+ # * <tt>:strict</tt> - Specifies whether validation should be strict.
42
+ # See <tt>Poncho::Validation#validates!</tt> for more information.
43
+ def validates_presence_of(*attr_names)
44
+ options = attr_names.last.is_a?(::Hash) ? attr_names.pop : {}
45
+ validates_with PresenceValidator, options.merge(:attributes => attr_names)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,172 @@
1
+ module Poncho
2
+ #
3
+ # class Person
4
+ # include Poncho::Validations
5
+ # validates_with MyValidator
6
+ # end
7
+ #
8
+ # class MyValidator < Poncho::Validator
9
+ # def validate(record)
10
+ # if some_complex_logic
11
+ # record.errors[:base] = "This record is invalid"
12
+ # end
13
+ # end
14
+ #
15
+ # private
16
+ # def some_complex_logic
17
+ # # ...
18
+ # end
19
+ # end
20
+ #
21
+ # Any class that inherits from Poncho::Validator must implement a method
22
+ # called <tt>validate</tt> which accepts a <tt>record</tt>.
23
+ #
24
+ # class Person
25
+ # include Poncho::Validations
26
+ # validates_with MyValidator
27
+ # end
28
+ #
29
+ # class MyValidator < Poncho::Validator
30
+ # def validate(record)
31
+ # record # => The person instance being validated
32
+ # options # => Any non-standard options passed to validates_with
33
+ # end
34
+ # end
35
+ #
36
+ # To cause a validation error, you must add to the <tt>record</tt>'s errors directly
37
+ # from within the validators message
38
+ #
39
+ # class MyValidator < Poncho::Validator
40
+ # def validate(record)
41
+ # record.errors.add :base, "This is some custom error message"
42
+ # record.errors.add :first_name, "This is some complex validation"
43
+ # # etc...
44
+ # end
45
+ # end
46
+ #
47
+ # To add behavior to the initialize method, use the following signature:
48
+ #
49
+ # class MyValidator < Poncho::Validator
50
+ # def initialize(options)
51
+ # super
52
+ # @my_custom_field = options[:field_name] || :first_name
53
+ # end
54
+ # end
55
+ #
56
+ # The easiest way to add custom validators for validating individual attributes
57
+ # is with the convenient <tt>Poncho::EachValidator</tt>. For example:
58
+ #
59
+ # class TitleValidator < Poncho::EachValidator
60
+ # def validate_each(record, attribute, value)
61
+ # record.errors.add attribute, 'must be Mr. Mrs. or Dr.' unless value.in?(['Mr.', 'Mrs.', 'Dr.'])
62
+ # end
63
+ # end
64
+ #
65
+ # This can now be used in combination with the +validates+ method
66
+ # (see <tt>Poncho::Validations::ClassMethods.validates</tt> for more on this)
67
+ #
68
+ # class Person
69
+ # include Poncho::Validations
70
+ # attr_accessor :title
71
+ #
72
+ # validates :title, :presence => true
73
+ # end
74
+ #
75
+ # Validator may also define a +setup+ instance method which will get called
76
+ # with the class that using that validator as its argument. This can be
77
+ # useful when there are prerequisites such as an +attr_accessor+ being present
78
+ # for example:
79
+ #
80
+ # class MyValidator < Poncho::Validator
81
+ # def setup(klass)
82
+ # klass.send :attr_accessor, :custom_attribute
83
+ # end
84
+ # end
85
+ #
86
+ # This setup method is only called when used with validation macros or the
87
+ # class level <tt>validates_with</tt> method.
88
+ #
89
+ class Validator
90
+ def self.kind
91
+ @kind ||= begin
92
+ full_name = name.split('::').last
93
+ full_name = full_name.gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
94
+ full_name.sub(/_validator$/, '').to_sym
95
+ end
96
+ end
97
+
98
+ attr_reader :options
99
+
100
+ # Accepts options that will be made available through the +options+ reader.
101
+ def initialize(options)
102
+ @options = options.freeze
103
+ end
104
+
105
+ # Override this method in subclasses with validation logic, adding errors
106
+ # to the records +errors+ array where necessary.
107
+ def validate(record)
108
+ raise NotImplementedError, "Subclasses must implement a validate(record) method."
109
+ end
110
+
111
+ def kind
112
+ self.class.kind
113
+ end
114
+ end
115
+
116
+ # +EachValidator+ is a validator which iterates through the attributes given
117
+ # in the options hash invoking the <tt>validate_each</tt> method passing in the
118
+ # record, attribute and value.
119
+ #
120
+ # All Poncho validations are built on top of this validator.
121
+ class EachValidator < Validator
122
+ attr_reader :attributes
123
+
124
+ # Returns a new validator instance. All options will be available via the
125
+ # +options+ reader, however the <tt>:attributes</tt> option will be removed
126
+ # and instead be made available through the +attributes+ reader.
127
+ def initialize(options)
128
+ @attributes = Array(options.delete(:attributes))
129
+ raise ':attributes cannot be blank' if @attributes.empty?
130
+ super
131
+ check_validity!
132
+ end
133
+
134
+ # Performs validation on the supplied record. By default this will call
135
+ # +validates_each+ to determine validity therefore subclasses should
136
+ # override +validates_each+ with validation logic.
137
+ def validate(record)
138
+ attributes.each do |attribute|
139
+ value = record.read_attribute_for_validation(attribute)
140
+ next if (value.nil? && options[:allow_nil]) || (value == "" && options[:allow_blank])
141
+ validate_each(record, attribute, value)
142
+ end
143
+ end
144
+
145
+ # Override this method in subclasses with the validation logic, adding
146
+ # errors to the records +errors+ array where necessary.
147
+ def validate_each(record, attribute, value)
148
+ raise NotImplementedError, 'Subclasses must implement a validate_each(record, attribute, value) method'
149
+ end
150
+
151
+ # Hook method that gets called by the initializer allowing verification
152
+ # that the arguments supplied are valid. You could for example raise an
153
+ # +ArgumentError+ when invalid options are supplied.
154
+ def check_validity!
155
+ end
156
+ end
157
+
158
+ # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
159
+ # and call this block for each attribute being validated. +validates_each+ uses this validator.
160
+ class BlockValidator < EachValidator
161
+ def initialize(options, &block)
162
+ @block = block
163
+ super
164
+ end
165
+
166
+ private
167
+
168
+ def validate_each(record, attribute, value)
169
+ @block.call(record, attribute, value)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,3 @@
1
+ module Poncho
2
+ VERSION = "0.0.2"
3
+ end
data/poncho.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'poncho/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "poncho"
8
+ gem.version = Poncho::VERSION
9
+ gem.authors = ["Alex MacCaw"]
10
+ gem.email = ["alex@stripe.com"]
11
+ gem.description = %q{Poncho is an API to build REST APIs with a convenient DSL.}
12
+ gem.summary = %q{Poncho is an API to build APIs}
13
+ gem.homepage = "https://github.com/stripe/poncho"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,105 @@
1
+ require 'minitest/autorun'
2
+ require 'rack/mock'
3
+ require 'poncho'
4
+
5
+ class TestMethod < MiniTest::Unit::TestCase
6
+ def env(params = {})
7
+ Rack::MockRequest.env_for('http://api.com/charges', :params => params)
8
+ end
9
+
10
+ def setup
11
+ end
12
+
13
+ def test_that_integer_params_are_validated
14
+ method = Class.new(Poncho::Method) do
15
+ param :amount, :type => :integer
16
+ end
17
+
18
+ status, headers, body = method.call(env(:amount => nil))
19
+ assert_equal 406, status
20
+
21
+ status, headers, body = method.call(env(:amount => '1'))
22
+ assert_equal 200, status
23
+
24
+ status, headers, body = method.call(env(:amount => 'blah'))
25
+ assert_equal 406, status
26
+ end
27
+
28
+ def test_that_string_params_are_validated
29
+ method = Class.new(Poncho::Method) do
30
+ param :amount, :type => :string
31
+ end
32
+
33
+ status, headers, body = method.call(env(:amount => nil))
34
+ assert_equal 406, status
35
+
36
+ status, headers, body = method.call(env(:amount => 'blah'))
37
+ assert_equal 200, status
38
+ end
39
+
40
+ def test_presence_validation
41
+ method = Class.new(Poncho::Method) do
42
+ param :amount, :required => true
43
+ end
44
+
45
+ status, headers, body = method.call(env())
46
+ assert_equal 406, status
47
+
48
+ status, headers, body = method.call(env(:amount => 'test'))
49
+ assert_equal 200, status
50
+ end
51
+
52
+ def test_custom_param_conversion
53
+ custom_param = Class.new(Poncho::Param) do
54
+ def convert(value)
55
+ return true if value == 'custom'
56
+ return false
57
+ end
58
+ end
59
+
60
+ method = Class.new(Poncho::Method) do
61
+ param :currency, :type => custom_param
62
+
63
+ def invoke
64
+ halt param(:currency) == true ? 200 : 406
65
+ end
66
+ end
67
+
68
+ status, headers, body = method.call(env(:currency => 'notcustom'))
69
+ assert_equal 406, status
70
+
71
+ status, headers, body = method.call(env(:currency => 'custom'))
72
+ assert_equal 200, status
73
+ end
74
+
75
+ def test_custom_param_validation
76
+ custom_param = Class.new(Poncho::Param) do
77
+ def validate_each(record, name, value)
78
+ unless ['USD', 'GBP'].include?(value)
79
+ record.errors.add(name, :invalid_currency)
80
+ end
81
+ end
82
+ end
83
+
84
+ method = Class.new(Poncho::Method) do
85
+ param :currency, :type => custom_param
86
+ end
87
+
88
+ status, headers, body = method.call(env(:currency => 'RSU'))
89
+ assert_equal 406, status
90
+
91
+ status, headers, body = method.call(env(:currency => 'USD'))
92
+ assert_equal 200, status
93
+ end
94
+
95
+ def test_json_method_returns_json
96
+ method = Class.new(Poncho::JSONMethod) do
97
+ def invoke
98
+ {:some => 'stuff'}
99
+ end
100
+ end
101
+
102
+ status, headers, body = method.call(env())
103
+ assert_equal({:some => 'stuff'}.to_json, body.body.first)
104
+ end
105
+ end