poncho 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +302 -0
- data/Rakefile +8 -0
- data/examples/app.rb +80 -0
- data/lib/poncho.rb +27 -0
- data/lib/poncho/error.rb +75 -0
- data/lib/poncho/errors.rb +142 -0
- data/lib/poncho/filters.rb +52 -0
- data/lib/poncho/json_method.rb +47 -0
- data/lib/poncho/method.rb +271 -0
- data/lib/poncho/param.rb +29 -0
- data/lib/poncho/params.rb +92 -0
- data/lib/poncho/params/array.rb +15 -0
- data/lib/poncho/params/boolean.rb +20 -0
- data/lib/poncho/params/boolean_string.rb +19 -0
- data/lib/poncho/params/integer.rb +17 -0
- data/lib/poncho/params/object.rb +15 -0
- data/lib/poncho/params/resource.rb +27 -0
- data/lib/poncho/params/string.rb +15 -0
- data/lib/poncho/params/validations.rb +24 -0
- data/lib/poncho/request.rb +71 -0
- data/lib/poncho/resource.rb +80 -0
- data/lib/poncho/response.rb +28 -0
- data/lib/poncho/returns.rb +25 -0
- data/lib/poncho/validations.rb +198 -0
- data/lib/poncho/validations/exclusions.rb +77 -0
- data/lib/poncho/validations/format.rb +105 -0
- data/lib/poncho/validations/inclusions.rb +77 -0
- data/lib/poncho/validations/length.rb +123 -0
- data/lib/poncho/validations/presence.rb +49 -0
- data/lib/poncho/validator.rb +172 -0
- data/lib/poncho/version.rb +3 -0
- data/poncho.gemspec +19 -0
- data/test/poncho/test_method.rb +105 -0
- data/test/poncho/test_resource.rb +71 -0
- metadata +84 -0
data/lib/poncho/param.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Poncho
|
2
|
+
class Param
|
3
|
+
def self.type
|
4
|
+
@type ||= begin
|
5
|
+
full_name = name.split('::').last
|
6
|
+
full_name = full_name.gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
|
7
|
+
full_name.sub(/_param$/, '').to_sym
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :name, :options
|
12
|
+
|
13
|
+
def initialize(name, options = {})
|
14
|
+
@name = name.to_sym
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def type
|
19
|
+
self.class.type
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_each(record, attribute, value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def convert(value)
|
26
|
+
value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
VALIDATES_DEFAULT_KEYS = [:resource, :type, :required, :format, :in, :not_in, :length]
|
9
|
+
|
10
|
+
def params
|
11
|
+
@params ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def param(name, options = {})
|
15
|
+
type = options[:type]
|
16
|
+
type ||= options[:resource] ? :resource : :string
|
17
|
+
|
18
|
+
klass = param_for_type(type)
|
19
|
+
param = klass.new(name, options)
|
20
|
+
params[param.name] = param
|
21
|
+
|
22
|
+
create_validations_for(param)
|
23
|
+
|
24
|
+
param
|
25
|
+
end
|
26
|
+
|
27
|
+
def string(name, options = {})
|
28
|
+
param(name, options.merge(:type => :string))
|
29
|
+
end
|
30
|
+
|
31
|
+
def integer(name, options = {})
|
32
|
+
param(name, options.merge(:type => :integer))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def param_for_type(type)
|
38
|
+
return type if type.is_a?(Class)
|
39
|
+
name = type.to_s.split('_').map {|w| w.capitalize }.join
|
40
|
+
const_get("#{name}Param")
|
41
|
+
rescue NameError
|
42
|
+
raise ArgumentError, "Unknown param: #{type}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_validations_for(param)
|
46
|
+
attribute = param.name
|
47
|
+
|
48
|
+
unless param.options[:validates] == false
|
49
|
+
validates_param(attribute, :param => param)
|
50
|
+
end
|
51
|
+
|
52
|
+
if param.options[:required]
|
53
|
+
validates_presence_of(attribute)
|
54
|
+
end
|
55
|
+
|
56
|
+
if param.options[:format]
|
57
|
+
validates_format_of(attribute, :with => param.options[:format])
|
58
|
+
end
|
59
|
+
|
60
|
+
if param.options[:in]
|
61
|
+
validates_inclusion_of(attribute, :in => param.options[:in])
|
62
|
+
end
|
63
|
+
|
64
|
+
if param.options[:not_in]
|
65
|
+
validates_exclusion_of(attribute, :in => param.options[:not_in])
|
66
|
+
end
|
67
|
+
|
68
|
+
if param.options[:length]
|
69
|
+
length_options = case param.options[:length]
|
70
|
+
when Integer
|
71
|
+
{:minimum => 0, :maximum => param.options[:length]}
|
72
|
+
when Range
|
73
|
+
{:within => param.options[:length]}
|
74
|
+
when Hash
|
75
|
+
param.options[:length]
|
76
|
+
end
|
77
|
+
validates_length_of(attribute, length_options)
|
78
|
+
end
|
79
|
+
|
80
|
+
validators = param.options.reject {|key, _| VALIDATES_DEFAULT_KEYS.include?(key) }
|
81
|
+
validates(attribute, validators) if validators.any?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
alias :param_for_validation? :respond_to?
|
87
|
+
end
|
88
|
+
|
89
|
+
Dir[File.dirname(__FILE__) + "/params/*.rb"].sort.each do |path|
|
90
|
+
filename = File.basename(path)
|
91
|
+
require "poncho/params/#{filename}"
|
92
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
class ArrayParam < Param
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
if !value.is_a?(Array)
|
6
|
+
record.errors.add(attribute, :invalid_array, options.merge(:value => value))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def convert(value)
|
11
|
+
Array(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
class BooleanParam < Param
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
converted = convert(value)
|
6
|
+
|
7
|
+
if !(converted.is_a?(TrueClass) || converted.is_a?(FalseClass))
|
8
|
+
record.errors.add(attribute, :invalid_boolean, options.merge(:value => value))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def convert(value)
|
13
|
+
if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
14
|
+
return value
|
15
|
+
end
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
class BooleanStringParam < Param
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
converted = convert(value)
|
6
|
+
|
7
|
+
if !(converted.is_a?(TrueClass) || converted.is_a?(FalseClass))
|
8
|
+
record.errors.add(attribute, :invalid_boolean_string, options.merge(:value => value))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def convert(value)
|
13
|
+
return true if value == 'yes'
|
14
|
+
return false if value == 'no'
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
class IntegerParam < Param
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
unless convert(value).is_a?(Integer)
|
6
|
+
record.errors.add(attribute, :invalid_integer, options.merge(:value => value))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def convert(value)
|
11
|
+
Integer(value)
|
12
|
+
rescue TypeError, ArgumentError
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
class ObjectParam < Param
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
if !value || !value.is_a?(Object)
|
6
|
+
record.errors.add(attribute, :invalid_object, options.merge(:value => value))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def convert(value)
|
11
|
+
value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
class ResourceParam < Param
|
4
|
+
def initialize(name, options = {})
|
5
|
+
super
|
6
|
+
|
7
|
+
unless options[:resource]
|
8
|
+
throw ArgumentError, ':resource required'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate_each(record, attribute, value)
|
13
|
+
resource = convert(value)
|
14
|
+
|
15
|
+
unless resource.valid?
|
16
|
+
resource.errors.to_hash.each do |attr, messages|
|
17
|
+
record.messages[:"#{attribute}[#{attr}]"] |= messages
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def convert(value)
|
23
|
+
options[:resource].new(value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Params
|
3
|
+
class StringParam < Param
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
unless value.is_a?(String)
|
6
|
+
record.errors.add(attribute, :invalid_string, options.merge(:value => value))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def convert(value)
|
11
|
+
value && value.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Validations
|
3
|
+
class ParamValidator < EachValidator
|
4
|
+
def check_validity!
|
5
|
+
unless options[:param]
|
6
|
+
raise ArgumentError, ':param option required'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate_each(record, attribute, value)
|
11
|
+
if record.param_for_validation?(attribute)
|
12
|
+
options[:param].validate_each(record, attribute, value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module HelperMethods
|
18
|
+
def validates_param(*attr_names)
|
19
|
+
options = attr_names.last.is_a?(::Hash) ? attr_names.pop : {}
|
20
|
+
validates_with ParamValidator, options.merge(:attributes => attr_names)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Poncho
|
2
|
+
class Request < Rack::Request
|
3
|
+
# Returns an array of acceptable media types for the response
|
4
|
+
def accept
|
5
|
+
@env['poncho.accept'] ||= begin
|
6
|
+
entries = @env['HTTP_ACCEPT'].to_s.split(',')
|
7
|
+
entries.map { |e| accept_entry(e) }.sort_by(&:last).map(&:first)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def preferred_type(*types)
|
12
|
+
return accept.first if types.empty?
|
13
|
+
types.flatten!
|
14
|
+
accept.detect do |pattern|
|
15
|
+
type = types.detect { |t| File.fnmatch(pattern, t) }
|
16
|
+
return type if type
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
alias accept? preferred_type
|
21
|
+
alias secure? ssl?
|
22
|
+
|
23
|
+
def forwarded?
|
24
|
+
@env.include? 'HTTP_X_FORWARDED_HOST'
|
25
|
+
end
|
26
|
+
|
27
|
+
def safe?
|
28
|
+
get? or head? or options? or trace?
|
29
|
+
end
|
30
|
+
|
31
|
+
def idempotent?
|
32
|
+
safe? or put? or delete?
|
33
|
+
end
|
34
|
+
|
35
|
+
def params
|
36
|
+
@params ||= begin
|
37
|
+
params = super.merge(action_dispatch_params)
|
38
|
+
indifferent_params(params)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Enable string or symbol key access to the nested params hash.
|
45
|
+
def indifferent_params(params)
|
46
|
+
params = indifferent_hash.merge(params)
|
47
|
+
params.each do |key, value|
|
48
|
+
next unless value.is_a?(Hash)
|
49
|
+
params[key] = indifferent_params(value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creates a Hash with indifferent access.
|
54
|
+
def indifferent_hash
|
55
|
+
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Pass in params from Rails routing
|
59
|
+
def action_dispatch_params
|
60
|
+
action_dispatch_params = env['action_dispatch.request.path_parameters'] || {}
|
61
|
+
action_dispatch_params.inject({}) {|hash, (key, value)| hash[key.to_s] = value; hash }
|
62
|
+
end
|
63
|
+
|
64
|
+
def accept_entry(entry)
|
65
|
+
type, *options = entry.delete(' ').split(';')
|
66
|
+
quality = 0 # we sort smallest first
|
67
|
+
options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
|
68
|
+
[type, [quality, type.count('*'), 1 - options.size]]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Poncho
|
2
|
+
class Resource
|
3
|
+
include Params
|
4
|
+
include Validations
|
5
|
+
|
6
|
+
attr_reader :record
|
7
|
+
|
8
|
+
def initialize(record)
|
9
|
+
if record.nil?
|
10
|
+
raise ResourceValidationError, 'Invalid nil record'
|
11
|
+
end
|
12
|
+
|
13
|
+
@record = record
|
14
|
+
end
|
15
|
+
|
16
|
+
# Params
|
17
|
+
|
18
|
+
def param(name)
|
19
|
+
param = self.class.params[name]
|
20
|
+
raise Error, "Undefined param #{name}" unless param
|
21
|
+
param.convert(param_before_type_cast(name))
|
22
|
+
end
|
23
|
+
|
24
|
+
def param_before_type_cast(name)
|
25
|
+
record.send(name) if record.respond_to?(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :param?, :respond_to?
|
29
|
+
|
30
|
+
# Serialization
|
31
|
+
|
32
|
+
def each
|
33
|
+
[to_json]
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_json(options = nil)
|
37
|
+
as_json.to_json
|
38
|
+
end
|
39
|
+
|
40
|
+
def as_json(options = nil)
|
41
|
+
validate!
|
42
|
+
to_hash
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_hash
|
46
|
+
self.class.params.keys.inject({}) do |hash, key|
|
47
|
+
hash[key] = send(key)
|
48
|
+
hash
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
alias_method :describe, :to_hash
|
53
|
+
|
54
|
+
# Validation
|
55
|
+
|
56
|
+
# We want to validate an attribute if its
|
57
|
+
# uncoerced value is not nil
|
58
|
+
def param_for_validation?(name)
|
59
|
+
if respond_to?(name)
|
60
|
+
!send(name).nil?
|
61
|
+
else
|
62
|
+
!param_before_type_cast(name).nil?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
alias_method :read_attribute_for_validation, :send
|
67
|
+
|
68
|
+
def validate!
|
69
|
+
raise ResourceValidationError, errors.to_s unless valid?
|
70
|
+
end
|
71
|
+
|
72
|
+
def method_missing(method_symbol, *arguments) #:nodoc:
|
73
|
+
if self.class.params.keys.include?(method_symbol)
|
74
|
+
return param(method_symbol)
|
75
|
+
end
|
76
|
+
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Poncho
|
2
|
+
class Response < Rack::Response
|
3
|
+
def body=(value)
|
4
|
+
value = value.body while Rack::Response === value
|
5
|
+
@body = String === value ? [value.to_str] : value
|
6
|
+
end
|
7
|
+
|
8
|
+
def each
|
9
|
+
block_given? ? super : enum_for(:each)
|
10
|
+
end
|
11
|
+
|
12
|
+
def finish
|
13
|
+
if status.to_i / 100 == 1
|
14
|
+
headers.delete 'Content-Length'
|
15
|
+
headers.delete 'Content-Type'
|
16
|
+
elsif Array === body and not [204, 304].include?(status.to_i)
|
17
|
+
# if some other code has already set Content-Length, don't muck with it
|
18
|
+
# currently, this would be the static file-handler
|
19
|
+
headers['Content-Length'] ||= body.inject(0) { |l, p| l + Rack::Utils.bytesize(p) }.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
# Rack::Response#finish sometimes returns self as response body. We don't want that.
|
23
|
+
status, headers, result = super
|
24
|
+
result = body if result == self
|
25
|
+
[status, headers, result]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|