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/error.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
module Poncho
|
2
|
+
class Error < StandardError
|
3
|
+
def to_json
|
4
|
+
as_json.to_json
|
5
|
+
end
|
6
|
+
|
7
|
+
def type
|
8
|
+
inspect
|
9
|
+
end
|
10
|
+
|
11
|
+
def as_json
|
12
|
+
{:error => {:type => type, :message => message}}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ServerError < Error
|
17
|
+
def code
|
18
|
+
500
|
19
|
+
end
|
20
|
+
|
21
|
+
def type
|
22
|
+
:server_error
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
"Sorry, something went wrong. " +
|
27
|
+
"We've been notified about the problem."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ResourceValidationError < ServerError
|
32
|
+
end
|
33
|
+
|
34
|
+
class ClientError < Error
|
35
|
+
attr_reader :type, :message
|
36
|
+
|
37
|
+
def initialize(type = nil, message = nil)
|
38
|
+
@type = type || self.class.name
|
39
|
+
@message = message
|
40
|
+
end
|
41
|
+
|
42
|
+
def code
|
43
|
+
400
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class InvalidRequest < ClientError
|
48
|
+
end
|
49
|
+
|
50
|
+
class ValidationError < ClientError
|
51
|
+
attr_reader :errors
|
52
|
+
|
53
|
+
def initialize(errors)
|
54
|
+
@errors = errors
|
55
|
+
end
|
56
|
+
|
57
|
+
def code
|
58
|
+
406
|
59
|
+
end
|
60
|
+
|
61
|
+
def as_json
|
62
|
+
errors
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class NotFoundError < ClientError
|
67
|
+
def code
|
68
|
+
404
|
69
|
+
end
|
70
|
+
|
71
|
+
def type
|
72
|
+
:not_found
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Poncho
|
2
|
+
class Errors
|
3
|
+
attr_reader :messages
|
4
|
+
|
5
|
+
def initialize(base)
|
6
|
+
@base = base
|
7
|
+
@messages = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Clear the messages
|
11
|
+
def clear
|
12
|
+
messages.clear
|
13
|
+
end
|
14
|
+
|
15
|
+
# Do the error messages include an error with key +error+?
|
16
|
+
def include?(error)
|
17
|
+
(v = messages[error]) && v.any?
|
18
|
+
end
|
19
|
+
alias_method :has_key?, :include?
|
20
|
+
|
21
|
+
# Get messages for +key+
|
22
|
+
def get(key)
|
23
|
+
messages[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Set messages for +key+ to +value+
|
27
|
+
def set(key, value)
|
28
|
+
messages[key] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
# Delete messages for +key+
|
32
|
+
def delete(key)
|
33
|
+
messages.delete(key)
|
34
|
+
end
|
35
|
+
|
36
|
+
# When passed a symbol or a name of a method, returns an array of errors
|
37
|
+
# for the method.
|
38
|
+
#
|
39
|
+
# p.errors[:name] # => ["can not be nil"]
|
40
|
+
# p.errors['name'] # => ["can not be nil"]
|
41
|
+
def [](attribute)
|
42
|
+
get(attribute.to_sym) || set(attribute.to_sym, [])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Adds to the supplied attribute the supplied error message.
|
46
|
+
#
|
47
|
+
# p.errors[:name] = "must be set"
|
48
|
+
# p.errors[:name] # => ['must be set']
|
49
|
+
def []=(attribute, error)
|
50
|
+
self[attribute] << error
|
51
|
+
end
|
52
|
+
|
53
|
+
def each
|
54
|
+
[to_s]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the number of error messages.
|
58
|
+
#
|
59
|
+
# p.errors.add(:name, "can't be blank")
|
60
|
+
# p.errors.size # => 1
|
61
|
+
# p.errors.add(:name, "must be specified")
|
62
|
+
# p.errors.size # => 2
|
63
|
+
def size
|
64
|
+
values.flatten.size
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns all message values
|
68
|
+
def values
|
69
|
+
messages.values
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns all message keys
|
73
|
+
def keys
|
74
|
+
messages.keys
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
"Validation errors:\n " + full_messages.join(', ')
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array of error messages, with the attribute name included
|
82
|
+
#
|
83
|
+
# p.errors.add(:name, "can't be blank")
|
84
|
+
# p.errors.add(:name, "must be specified")
|
85
|
+
# p.errors.to_a # => ["name can't be blank", "name must be specified"]
|
86
|
+
def to_a
|
87
|
+
full_messages
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns the number of error messages.
|
91
|
+
# p.errors.add(:name, "can't be blank")
|
92
|
+
# p.errors.count # => 1
|
93
|
+
# p.errors.add(:name, "must be specified")
|
94
|
+
# p.errors.count # => 2
|
95
|
+
def count
|
96
|
+
to_a.size
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns true if no errors are found, false otherwise.
|
100
|
+
# If the error message is a string it can be empty.
|
101
|
+
def empty?
|
102
|
+
messages.all? { |k, v| v && v == "" && !v.is_a?(String) }
|
103
|
+
end
|
104
|
+
alias_method :blank?, :empty?
|
105
|
+
|
106
|
+
# Return the first error we get
|
107
|
+
def as_json(options=nil)
|
108
|
+
return {} if messages.empty?
|
109
|
+
attribute, types = messages.first
|
110
|
+
type = types.first
|
111
|
+
|
112
|
+
{
|
113
|
+
:error => {
|
114
|
+
:param => attribute,
|
115
|
+
:type => type,
|
116
|
+
:message => nil
|
117
|
+
}
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_json(options=nil)
|
122
|
+
as_json.to_json
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_hash
|
126
|
+
messages.dup
|
127
|
+
end
|
128
|
+
|
129
|
+
def add(attribute, message = nil, options = {})
|
130
|
+
self[attribute] << message
|
131
|
+
end
|
132
|
+
|
133
|
+
def full_messages
|
134
|
+
messages.map { |attribute, message| full_message(attribute, message) }
|
135
|
+
end
|
136
|
+
|
137
|
+
def full_message(attribute, message)
|
138
|
+
return message if attribute == :base
|
139
|
+
"#{attribute} #{message.join(', ')}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Poncho
|
2
|
+
module Filters
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
extend ClassMethods
|
6
|
+
include InstanceMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Filter
|
11
|
+
attr_reader :options, :block
|
12
|
+
|
13
|
+
def initialize(options = {}, &block)
|
14
|
+
@options = options
|
15
|
+
@block = block
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_proc
|
19
|
+
block.to_proc
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(*args)
|
23
|
+
block.call(*args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module InstanceMethods
|
28
|
+
def run_filters(type)
|
29
|
+
self.class.run_filters(type, self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def filters
|
35
|
+
@filters ||= Hash.new {[]}
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_filter(type, options = {}, &block)
|
39
|
+
filters[type] << Filter.new(options, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_filters(type, binding = self)
|
43
|
+
base = self
|
44
|
+
|
45
|
+
while base.respond_to?(:filters)
|
46
|
+
base.filters[type].each {|f| binding.instance_eval(&f) }
|
47
|
+
base = base.superclass
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
begin
|
2
|
+
require 'json'
|
3
|
+
rescue LoadError => e
|
4
|
+
require 'json/pure'
|
5
|
+
end
|
6
|
+
|
7
|
+
module Poncho
|
8
|
+
class JSONMethod < Method
|
9
|
+
error 500 do
|
10
|
+
json ServerError.new
|
11
|
+
end
|
12
|
+
|
13
|
+
error 403 do
|
14
|
+
json InvalidRequestError.new
|
15
|
+
end
|
16
|
+
|
17
|
+
error 404 do
|
18
|
+
json NotFoundError.new
|
19
|
+
end
|
20
|
+
|
21
|
+
error ValidationError do
|
22
|
+
json env['poncho.error'].errors
|
23
|
+
end
|
24
|
+
|
25
|
+
def body(value = nil)
|
26
|
+
if value && !json_content_type?
|
27
|
+
content_type :json
|
28
|
+
value = value.to_json
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def json(value)
|
35
|
+
content_type :json
|
36
|
+
body(value.to_json)
|
37
|
+
end
|
38
|
+
|
39
|
+
def json?
|
40
|
+
request.accept?(mime_type(:json))
|
41
|
+
end
|
42
|
+
|
43
|
+
def json_content_type?
|
44
|
+
response['Content-Type'] == mime_type(:json)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
module Poncho
|
2
|
+
class Method
|
3
|
+
include Validations
|
4
|
+
include Filters
|
5
|
+
include Params
|
6
|
+
|
7
|
+
def self.call(env, params = {})
|
8
|
+
self.new.call(env, params)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Some magic so you can do one-line
|
12
|
+
# Sinatra routes. For example:
|
13
|
+
# get '/charges', &ChargesListMethod
|
14
|
+
def self.to_proc
|
15
|
+
this = self
|
16
|
+
Proc.new { this.call(env, params) }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Filters
|
20
|
+
|
21
|
+
def self.before(options = {}, &block)
|
22
|
+
add_filter(:before, options, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.before_validation(options = {}, &block)
|
26
|
+
add_filter(:before_validation, options, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.errors
|
30
|
+
@errors ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.error(type = :base, &block)
|
34
|
+
errors[type] = block
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.helpers(*extensions, &block)
|
38
|
+
class_eval(&block) if block_given?
|
39
|
+
include(*extensions) if extensions.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :env, :request, :response
|
43
|
+
|
44
|
+
def call(env, params = {})
|
45
|
+
@env = env
|
46
|
+
@request = Request.new(env)
|
47
|
+
@response = Response.new
|
48
|
+
|
49
|
+
# Extra params, say from Sinatra's routing
|
50
|
+
@request.params.merge!(params)
|
51
|
+
|
52
|
+
wrap {
|
53
|
+
validate!
|
54
|
+
dispatch!
|
55
|
+
}
|
56
|
+
|
57
|
+
unless @response['Content-Type']
|
58
|
+
if Array === body and body[0].respond_to? :content_type
|
59
|
+
content_type body[0].content_type
|
60
|
+
else
|
61
|
+
content_type :html
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
@response.finish
|
66
|
+
end
|
67
|
+
|
68
|
+
def headers(hash=nil)
|
69
|
+
response.headers.merge! hash if hash
|
70
|
+
response.headers
|
71
|
+
end
|
72
|
+
|
73
|
+
def params
|
74
|
+
request.params.inject({}) do |hash, (key, _)|
|
75
|
+
hash[key.to_sym] = param(key)
|
76
|
+
hash
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def param(name)
|
81
|
+
value = param_before_type_cast(name)
|
82
|
+
param = self.class.params[name.to_sym]
|
83
|
+
param ? param.convert(value) : value
|
84
|
+
end
|
85
|
+
|
86
|
+
def param?(name)
|
87
|
+
request.params.has_key?(name.to_s)
|
88
|
+
end
|
89
|
+
|
90
|
+
def param_before_type_cast(name)
|
91
|
+
request.params[name.to_s]
|
92
|
+
end
|
93
|
+
|
94
|
+
def status(value=nil)
|
95
|
+
response.status = value if value
|
96
|
+
response.status
|
97
|
+
end
|
98
|
+
|
99
|
+
def redirect(uri, *args)
|
100
|
+
if env['HTTP_VERSION'] == 'HTTP/1.1' and env['REQUEST_METHOD'] != 'GET'
|
101
|
+
status 303
|
102
|
+
else
|
103
|
+
status 302
|
104
|
+
end
|
105
|
+
response['Location'] = uri
|
106
|
+
halt(*args)
|
107
|
+
end
|
108
|
+
|
109
|
+
def content_type(type = nil, params = {})
|
110
|
+
return response['Content-Type'] unless type
|
111
|
+
default = params.delete :default
|
112
|
+
mime_type = mime_type(type) || default
|
113
|
+
fail "Unknown media type: %p" % type if mime_type.nil?
|
114
|
+
response['Content-Type'] = mime_type.dup
|
115
|
+
end
|
116
|
+
|
117
|
+
def body(value = nil, &block)
|
118
|
+
if block_given?
|
119
|
+
def block.each; yield(call) end
|
120
|
+
response.body = block
|
121
|
+
elsif value
|
122
|
+
response.body = value
|
123
|
+
else
|
124
|
+
response.body
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Statuses
|
129
|
+
|
130
|
+
# whether or not the status is set to 2xx
|
131
|
+
def success?
|
132
|
+
status.between? 200, 299
|
133
|
+
end
|
134
|
+
|
135
|
+
# whether or not the status is set to 3xx
|
136
|
+
def redirect?
|
137
|
+
status.between? 300, 399
|
138
|
+
end
|
139
|
+
|
140
|
+
# whether or not the status is set to 4xx
|
141
|
+
def client_error?
|
142
|
+
status.between? 400, 499
|
143
|
+
end
|
144
|
+
|
145
|
+
# whether or not the status is set to 5xx
|
146
|
+
def server_error?
|
147
|
+
status.between? 500, 599
|
148
|
+
end
|
149
|
+
|
150
|
+
# whether or not the status is set to 404
|
151
|
+
def not_found?
|
152
|
+
status == 404
|
153
|
+
end
|
154
|
+
|
155
|
+
# Errors
|
156
|
+
|
157
|
+
def halt(*response)
|
158
|
+
response = response.first if response.length == 1
|
159
|
+
throw :halt, response
|
160
|
+
end
|
161
|
+
|
162
|
+
def error(code, body=nil)
|
163
|
+
code, body = 500, code.to_str if code.respond_to? :to_str
|
164
|
+
self.body(body) unless body.nil?
|
165
|
+
halt code
|
166
|
+
end
|
167
|
+
|
168
|
+
def not_found(body=nil)
|
169
|
+
error 404, body
|
170
|
+
end
|
171
|
+
|
172
|
+
# Implement
|
173
|
+
|
174
|
+
def invoke
|
175
|
+
end
|
176
|
+
|
177
|
+
# Validation
|
178
|
+
|
179
|
+
alias_method :read_attribute_for_validation, :param_before_type_cast
|
180
|
+
alias_method :param_for_validation?, :param?
|
181
|
+
|
182
|
+
protected
|
183
|
+
|
184
|
+
def validate!
|
185
|
+
run_filters :before_validation
|
186
|
+
run_extra_param_validations!
|
187
|
+
run_validations!
|
188
|
+
raise ValidationError.new(errors) unless errors.empty?
|
189
|
+
ensure
|
190
|
+
run_filters :after_validation
|
191
|
+
end
|
192
|
+
|
193
|
+
DEFAULT_PARAMS = %w{splat captures action controller}
|
194
|
+
|
195
|
+
def run_extra_param_validations!
|
196
|
+
(request.params.keys - DEFAULT_PARAMS).each do |param|
|
197
|
+
unless self.class.params.has_key?(param.to_sym)
|
198
|
+
errors.add(param, :invalid_param)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Calling
|
204
|
+
|
205
|
+
def dispatch!
|
206
|
+
run_filters :before
|
207
|
+
invoke
|
208
|
+
ensure
|
209
|
+
run_filters :after
|
210
|
+
end
|
211
|
+
|
212
|
+
def error_block(key)
|
213
|
+
base = self.class
|
214
|
+
|
215
|
+
while base.respond_to?(:errors)
|
216
|
+
block = base.errors[key]
|
217
|
+
return block if block
|
218
|
+
base = base.superclass
|
219
|
+
end
|
220
|
+
|
221
|
+
return false unless key.respond_to?(:superclass) && key.superclass < Exception
|
222
|
+
error_block(key.superclass)
|
223
|
+
end
|
224
|
+
|
225
|
+
def handle_exception!(error)
|
226
|
+
env['poncho.error'] = error
|
227
|
+
|
228
|
+
status error.respond_to?(:code) ? Integer(error.code) : 500
|
229
|
+
|
230
|
+
if server_error?
|
231
|
+
request.logger.error(
|
232
|
+
"#{error.class}: #{error}\n\t" +
|
233
|
+
error.backtrace.join("\n\t")
|
234
|
+
)
|
235
|
+
end
|
236
|
+
|
237
|
+
block = error_block(error.class)
|
238
|
+
block ||= error_block(status)
|
239
|
+
block ||= error_block(:base)
|
240
|
+
|
241
|
+
if block
|
242
|
+
wrap {
|
243
|
+
instance_eval(&block)
|
244
|
+
}
|
245
|
+
else
|
246
|
+
raise error if server_error?
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def wrap
|
251
|
+
res = catch(:halt) { yield }
|
252
|
+
res = [res] if Fixnum === res or String === res
|
253
|
+
if Array === res and Fixnum === res.first
|
254
|
+
status(res.shift)
|
255
|
+
body(res.pop)
|
256
|
+
headers(*res)
|
257
|
+
elsif res.respond_to? :each
|
258
|
+
body res
|
259
|
+
end
|
260
|
+
rescue ::Exception => e
|
261
|
+
handle_exception!(e)
|
262
|
+
end
|
263
|
+
|
264
|
+
def mime_type(type, value=nil)
|
265
|
+
return type if type.nil? || type.to_s.include?('/')
|
266
|
+
type = ".#{type}" unless type.to_s[0] == ?.
|
267
|
+
return Rack::Mime.mime_type(type, nil) unless value
|
268
|
+
Rack::Mime::MIME_TYPES[type] = value
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|