poncho 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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