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