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