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