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,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