gin 0.0.0 → 1.0.0
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/.autotest +3 -3
- data/.gitignore +7 -0
- data/History.rdoc +3 -6
- data/Manifest.txt +36 -2
- data/README.rdoc +24 -14
- data/Rakefile +2 -9
- data/lib/gin.rb +122 -1
- data/lib/gin/app.rb +595 -0
- data/lib/gin/config.rb +50 -0
- data/lib/gin/controller.rb +602 -0
- data/lib/gin/core_ext/cgi.rb +15 -0
- data/lib/gin/core_ext/gin_class.rb +10 -0
- data/lib/gin/errorable.rb +113 -0
- data/lib/gin/filterable.rb +200 -0
- data/lib/gin/reloadable.rb +90 -0
- data/lib/gin/request.rb +76 -0
- data/lib/gin/response.rb +51 -0
- data/lib/gin/router.rb +222 -0
- data/lib/gin/stream.rb +56 -0
- data/public/400.html +14 -0
- data/public/404.html +13 -0
- data/public/500.html +14 -0
- data/public/error.html +38 -0
- data/public/favicon.ico +0 -0
- data/public/gin.css +61 -0
- data/public/gin_sm.png +0 -0
- data/test/app/app_foo.rb +15 -0
- data/test/app/controllers/app_controller.rb +16 -0
- data/test/app/controllers/foo_controller.rb +3 -0
- data/test/mock_config/backend.yml +7 -0
- data/test/mock_config/memcache.yml +10 -0
- data/test/mock_config/not_a_config.txt +0 -0
- data/test/test_app.rb +592 -0
- data/test/test_config.rb +33 -0
- data/test/test_controller.rb +808 -0
- data/test/test_errorable.rb +221 -0
- data/test/test_filterable.rb +126 -0
- data/test/test_gin.rb +59 -0
- data/test/test_helper.rb +5 -0
- data/test/test_request.rb +81 -0
- data/test/test_response.rb +68 -0
- data/test/test_router.rb +193 -0
- metadata +80 -15
- data/bin/gin +0 -3
- data/test/gin_test.rb +0 -8
data/lib/gin/config.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class Gin::Config
|
4
|
+
|
5
|
+
attr_reader :dir
|
6
|
+
|
7
|
+
def initialize environment, dir=nil
|
8
|
+
self.dir = dir
|
9
|
+
@environment = environment
|
10
|
+
@meta = class << self; self; end
|
11
|
+
@data = {}
|
12
|
+
self.load!
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def dir= val
|
17
|
+
@dir = File.join(val, "*.yml") if val
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def load!
|
22
|
+
return unless @dir
|
23
|
+
Dir[@dir].each do |filepath|
|
24
|
+
c = YAML.load_file(filepath)
|
25
|
+
c = (c['default'] || {}).merge (c[@environment] || {})
|
26
|
+
|
27
|
+
name = File.basename(filepath, ".yml")
|
28
|
+
set name, c
|
29
|
+
end
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def set name, data
|
35
|
+
@data[name] = data
|
36
|
+
define_method(name){ @data[name] } unless respond_to? name
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def has? name
|
41
|
+
@data.has_key?(name) && respond_to?(name)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def define_method name, &block
|
48
|
+
@meta.send :define_method, name, &block
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,602 @@
|
|
1
|
+
class Gin::Controller
|
2
|
+
extend GinClass
|
3
|
+
include Gin::Filterable
|
4
|
+
include Gin::Errorable
|
5
|
+
|
6
|
+
|
7
|
+
error Gin::NotFound, Gin::BadRequest, ::Exception do |err|
|
8
|
+
status( err.respond_to?(:http_status) ? err.http_status : 500 )
|
9
|
+
@response.headers.clear
|
10
|
+
content_type :html
|
11
|
+
body html_error_page(err)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
##
|
16
|
+
# Array of action names for this controller.
|
17
|
+
|
18
|
+
def self.actions
|
19
|
+
instance_methods(false)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
##
|
24
|
+
# String representing the controller name.
|
25
|
+
# Underscores the class name and removes mentions of 'controller'.
|
26
|
+
# MyApp::FooController.controller_name
|
27
|
+
# #=> "my_app/foo"
|
28
|
+
|
29
|
+
def self.controller_name
|
30
|
+
@ctrl_name ||= Gin.underscore(self.to_s).gsub(/_?controller_?/,'')
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
##
|
35
|
+
# Set or get the default content type for this Gin::Controller.
|
36
|
+
# Default value is "text/html". This attribute is inherited.
|
37
|
+
|
38
|
+
def self.content_type new_type=nil
|
39
|
+
return @content_type = new_type if new_type
|
40
|
+
return @content_type if @content_type
|
41
|
+
self.superclass.respond_to?(:content_type) ?
|
42
|
+
self.superclass.content_type.dup : "text/html"
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
##
|
47
|
+
# Execute arbitrary code in the context of a Gin::Controller instance.
|
48
|
+
# Returns a Rack response Array.
|
49
|
+
|
50
|
+
def self.exec app, env, &block
|
51
|
+
inst = new(app, env)
|
52
|
+
inst.invoke{ inst.instance_exec(&block) }
|
53
|
+
inst.response.finish
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
class_proxy :controller_name
|
58
|
+
|
59
|
+
attr_reader :app, :request, :response, :action, :env
|
60
|
+
|
61
|
+
|
62
|
+
def initialize app, env
|
63
|
+
@app = app
|
64
|
+
@action = nil
|
65
|
+
@env = env
|
66
|
+
@request = Gin::Request.new env
|
67
|
+
@response = Gin::Response.new
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def call_action action #:nodoc:
|
72
|
+
invoke{ dispatch action }
|
73
|
+
invoke{ handle_status(@response.status) }
|
74
|
+
content_type self.class.content_type unless
|
75
|
+
@response[Gin::Response::H_CTYPE]
|
76
|
+
@response.finish
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
##
|
81
|
+
# Set or get the HTTP response status code.
|
82
|
+
|
83
|
+
def status code=nil
|
84
|
+
@response.status = code if code
|
85
|
+
@response.status
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
##
|
90
|
+
# Get or set the HTTP response body.
|
91
|
+
|
92
|
+
def body body=nil
|
93
|
+
@response.body = body if body
|
94
|
+
@response.body
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
##
|
99
|
+
# Get the normalized mime-type matching the given input.
|
100
|
+
|
101
|
+
def mime_type type
|
102
|
+
@app.mime_type type
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
##
|
107
|
+
# Get or set the HTTP response Content-Type header.
|
108
|
+
|
109
|
+
def content_type type=nil, params={}
|
110
|
+
return @response[Gin::Response::H_CTYPE] unless type
|
111
|
+
|
112
|
+
default = params.delete(:default)
|
113
|
+
mime_type = mime_type(type) || default
|
114
|
+
raise "Unknown media type: %p" % type if mime_type.nil?
|
115
|
+
|
116
|
+
mime_type = mime_type.dup
|
117
|
+
unless params.include? :charset
|
118
|
+
params[:charset] = params.delete('charset') || 'UTF-8'
|
119
|
+
end
|
120
|
+
|
121
|
+
params.delete :charset if mime_type.include? 'charset'
|
122
|
+
unless params.empty?
|
123
|
+
mime_type << (mime_type.include?(';') ? ', ' : ';')
|
124
|
+
mime_type << params.map do |key, val|
|
125
|
+
val = val.inspect if val =~ /[";,]/
|
126
|
+
"#{key}=#{val}"
|
127
|
+
end.join(', ')
|
128
|
+
end
|
129
|
+
|
130
|
+
@response[Gin::Response::H_CTYPE] = mime_type
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
##
|
135
|
+
# Stop the execution of an action and return the response.
|
136
|
+
# May be given a status code, string, header Hash, or a combination:
|
137
|
+
# halt 400, "Badly formed request"
|
138
|
+
# halt "Done early! WOOO!"
|
139
|
+
# halt 302, {'Location' => 'http://example.com'}, "You are being redirected"
|
140
|
+
|
141
|
+
def halt *resp
|
142
|
+
resp = resp.first if resp.length == 1
|
143
|
+
throw :halt, resp
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
##
|
148
|
+
# Halt processing and return the error status provided.
|
149
|
+
|
150
|
+
def error code, body=nil
|
151
|
+
code, body = 500, code if code.respond_to? :to_str
|
152
|
+
@response.body = body unless body.nil?
|
153
|
+
halt code
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
##
|
158
|
+
# Set the ETag header. If the ETag was set in a previous request
|
159
|
+
# and matches the current one, halts the action and returns a 304
|
160
|
+
# on GET and HEAD requests.
|
161
|
+
|
162
|
+
def etag value, opts={}
|
163
|
+
opts = {:kind => opts} unless Hash === opts
|
164
|
+
kind = opts[:kind] || :strong
|
165
|
+
new_resource = opts.fetch(:new_resource) { @request.post? }
|
166
|
+
|
167
|
+
unless [:strong, :weak].include?(kind)
|
168
|
+
raise ArgumentError, ":strong or :weak expected"
|
169
|
+
end
|
170
|
+
|
171
|
+
value = '"%s"' % value
|
172
|
+
value = 'W/' + value if kind == :weak
|
173
|
+
@response['ETag'] = value
|
174
|
+
|
175
|
+
if (200..299).include?(status) || status == 304
|
176
|
+
if etag_matches? @env['HTTP_IF_NONE_MATCH'], new_resource
|
177
|
+
halt(@request.safe? ? 304 : 412)
|
178
|
+
end
|
179
|
+
|
180
|
+
if @env['HTTP_IF_MATCH']
|
181
|
+
halt 412 unless etag_matches? @env['HTTP_IF_MATCH'], new_resource
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
def etag_matches? list, new_resource=@request.post? #:nodoc:
|
188
|
+
return !new_resource if list == '*'
|
189
|
+
list.to_s.split(/\s*,\s*/).include? response['ETag']
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
|
194
|
+
|
195
|
+
##
|
196
|
+
# Set multiple response headers with Hash.
|
197
|
+
|
198
|
+
def headers hash=nil
|
199
|
+
@response.headers.merge! hash if hash
|
200
|
+
@response.headers
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
##
|
205
|
+
# Assigns a Gin::Stream to the response body, which is yielded to the block.
|
206
|
+
# The block execution is delayed until the action returns.
|
207
|
+
# stream do |io|
|
208
|
+
# file = File.open "somefile", "rb"
|
209
|
+
# io << file.read(1024) until file.eof?
|
210
|
+
# file.close
|
211
|
+
# end
|
212
|
+
|
213
|
+
def stream keep_open=false, &block
|
214
|
+
scheduler = env['async.callback'] ? EventMachine : Gin::Stream
|
215
|
+
body Gin::Stream.new(scheduler, keep_open){ |out| yield(out) }
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
##
|
220
|
+
# Accessor for main application logger.
|
221
|
+
|
222
|
+
def logger
|
223
|
+
@app.logger
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
##
|
228
|
+
# Get the request params.
|
229
|
+
|
230
|
+
def params
|
231
|
+
@request.params
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
##
|
236
|
+
# Access the request session.
|
237
|
+
|
238
|
+
def session
|
239
|
+
@request.session
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
##
|
244
|
+
# Access the request cookies.
|
245
|
+
|
246
|
+
def cookies
|
247
|
+
@request.cookies
|
248
|
+
end
|
249
|
+
|
250
|
+
|
251
|
+
##
|
252
|
+
# Build a path to the given controller and action or route name,
|
253
|
+
# with any expected params. If no controller is specified and the
|
254
|
+
# current controller responds to the symbol given, uses the current
|
255
|
+
# controller for path lookup.
|
256
|
+
#
|
257
|
+
# path_to FooController, :show, :id => 123
|
258
|
+
# #=> "/foo/123"
|
259
|
+
#
|
260
|
+
# # From FooController
|
261
|
+
# path_to :show, :id => 123
|
262
|
+
# #=> "/foo/123"
|
263
|
+
#
|
264
|
+
# path_to :show_foo, :id => 123
|
265
|
+
# #=> "/foo/123"
|
266
|
+
|
267
|
+
def path_to *args
|
268
|
+
return "#{args[0]}#{"?" << Gin.build_query(args[1]) if args[1]}" if String === args[0]
|
269
|
+
args.unshift(self.class) if Symbol === args[0] && respond_to?(args[0])
|
270
|
+
@app.router.path_to(*args)
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
##
|
275
|
+
# Build a URI to the given controller and action or named route, or path,
|
276
|
+
# with any expected params.
|
277
|
+
# url_to "/foo"
|
278
|
+
# #=> "http://example.com/foo
|
279
|
+
#
|
280
|
+
# url_to "/foo", :page => 2
|
281
|
+
# #=> "http://example.com/foo?page=foo
|
282
|
+
#
|
283
|
+
# url_to MyController, :action
|
284
|
+
# #=> "http://example.com/routed/action
|
285
|
+
#
|
286
|
+
# url_to MyController, :show, :id => 123
|
287
|
+
# #=> "http://example.com/routed/action/123
|
288
|
+
#
|
289
|
+
# url_to :show_foo
|
290
|
+
# #=> "http://example.com/routed/action
|
291
|
+
|
292
|
+
|
293
|
+
def url_to *args
|
294
|
+
path = path_to(*args)
|
295
|
+
|
296
|
+
return path if path =~ /\A[A-z][A-z0-9\+\.\-]*:/
|
297
|
+
|
298
|
+
uri = [host = ""]
|
299
|
+
host << "http#{'s' if @request.ssl?}://"
|
300
|
+
|
301
|
+
if @request.forwarded? || @request.port != (@request.ssl? ? 443 : 80)
|
302
|
+
host << @request.host_with_port
|
303
|
+
else
|
304
|
+
host << @request.host
|
305
|
+
end
|
306
|
+
|
307
|
+
uri << @request.script_name.to_s
|
308
|
+
uri << path
|
309
|
+
File.join uri
|
310
|
+
end
|
311
|
+
|
312
|
+
alias to url_to
|
313
|
+
|
314
|
+
|
315
|
+
##
|
316
|
+
# Send a 301, 302, or 303 redirect and halt.
|
317
|
+
# Supports passing a full URI, partial path.
|
318
|
+
# redirect "http://google.com"
|
319
|
+
# redirect "/foo"
|
320
|
+
# redirect "/foo", 301, "You are being redirected..."
|
321
|
+
# redirect to(MyController, :action, :id => 123)
|
322
|
+
# redirect to(:show_foo, :id => 123)
|
323
|
+
|
324
|
+
def redirect uri, *args
|
325
|
+
if @env['HTTP_VERSION'] == 'HTTP/1.1' && @env["REQUEST_METHOD"] != 'GET'
|
326
|
+
status 303
|
327
|
+
else
|
328
|
+
status 302
|
329
|
+
end
|
330
|
+
|
331
|
+
@response['Location'] = url_to(uri.to_s)
|
332
|
+
halt(*args)
|
333
|
+
end
|
334
|
+
|
335
|
+
|
336
|
+
##
|
337
|
+
# Assigns a file to the response body and halts the execution of the action.
|
338
|
+
# Produces a 404 response if no file is found.
|
339
|
+
|
340
|
+
def send_file path, opts={}
|
341
|
+
if opts[:type] || !@response[Gin::Response::H_CTYPE]
|
342
|
+
content_type opts[:type] || File.extname(path),
|
343
|
+
:default => 'application/octet-stream'
|
344
|
+
end
|
345
|
+
|
346
|
+
disposition = opts[:disposition]
|
347
|
+
filename = opts[:filename]
|
348
|
+
disposition = 'attachment' if disposition.nil? && filename
|
349
|
+
filename = File.basename(path) if filename.nil?
|
350
|
+
|
351
|
+
if disposition
|
352
|
+
@response['Content-Disposition'] =
|
353
|
+
"%s; filename=\"%s\"" % [disposition, filename]
|
354
|
+
end
|
355
|
+
|
356
|
+
last_modified opts[:last_modified] || File.mtime(path).httpdate
|
357
|
+
halt 200 if @request.head?
|
358
|
+
|
359
|
+
@response['Content-Length'] = File.size?(path).to_s
|
360
|
+
halt 200, File.open(path, "rb")
|
361
|
+
|
362
|
+
rescue Errno::ENOENT
|
363
|
+
halt 404
|
364
|
+
end
|
365
|
+
|
366
|
+
|
367
|
+
##
|
368
|
+
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
|
369
|
+
# and halt if conditional GET matches. The +time+ argument is a Time,
|
370
|
+
# DateTime, or other object that responds to +to_time+ or +httpdate+.
|
371
|
+
|
372
|
+
def last_modified time
|
373
|
+
return unless time
|
374
|
+
|
375
|
+
time = Time.at(time) if Integer === time
|
376
|
+
time = Time.parse(time) if String === time
|
377
|
+
time = time.to_time if time.respond_to?(:to_time)
|
378
|
+
|
379
|
+
@response['Last-Modified'] = time.httpdate
|
380
|
+
return if @env['HTTP_IF_NONE_MATCH']
|
381
|
+
|
382
|
+
if status == 200 && @env['HTTP_IF_MODIFIED_SINCE']
|
383
|
+
# compare based on seconds since epoch
|
384
|
+
since = Time.httpdate(@env['HTTP_IF_MODIFIED_SINCE']).to_i
|
385
|
+
halt 304 if since >= time.to_i
|
386
|
+
end
|
387
|
+
|
388
|
+
if @env['HTTP_IF_UNMODIFIED_SINCE'] &&
|
389
|
+
((200..299).include?(status) || status == 412)
|
390
|
+
|
391
|
+
# compare based on seconds since epoch
|
392
|
+
since = Time.httpdate(@env['HTTP_IF_UNMODIFIED_SINCE']).to_i
|
393
|
+
halt 412 if since < time.to_i
|
394
|
+
end
|
395
|
+
rescue ArgumentError
|
396
|
+
end
|
397
|
+
|
398
|
+
|
399
|
+
##
|
400
|
+
# Specify response freshness policy for HTTP caches (Cache-Control header).
|
401
|
+
# Any number of non-value directives (:public, :private, :no_cache,
|
402
|
+
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
|
403
|
+
# a Hash of value directives (:max_age, :min_stale, :s_max_age).
|
404
|
+
#
|
405
|
+
# cache_control :public, :must_revalidate, :max_age => 60
|
406
|
+
# #=> Cache-Control: public, must-revalidate, max-age=60
|
407
|
+
|
408
|
+
def cache_control *values
|
409
|
+
if Hash === values.last
|
410
|
+
hash = values.pop
|
411
|
+
hash.reject!{|k,v| v == false || v == true && values << k }
|
412
|
+
else
|
413
|
+
hash = {}
|
414
|
+
end
|
415
|
+
|
416
|
+
values.map! { |value| value.to_s.tr('_','-') }
|
417
|
+
hash.each do |key, value|
|
418
|
+
key = key.to_s.tr('_', '-')
|
419
|
+
value = value.to_i if key == "max-age"
|
420
|
+
values << [key, value].join('=')
|
421
|
+
end
|
422
|
+
|
423
|
+
@response['Cache-Control'] = values.join(', ') if values.any?
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
##
|
428
|
+
# Set the Expires header and Cache-Control/max-age directive. Amount
|
429
|
+
# can be an integer number of seconds in the future or a Time object
|
430
|
+
# indicating when the response should be considered "stale". The remaining
|
431
|
+
# "values" arguments are passed to the #cache_control helper:
|
432
|
+
#
|
433
|
+
# expires 500, :public, :must_revalidate
|
434
|
+
# => Cache-Control: public, must-revalidate, max-age=60
|
435
|
+
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT
|
436
|
+
|
437
|
+
def expires amount, *values
|
438
|
+
values << {} unless Hash === values.last
|
439
|
+
|
440
|
+
if Integer === amount
|
441
|
+
time = Time.now + amount.to_i
|
442
|
+
max_age = amount
|
443
|
+
else
|
444
|
+
time = String === amount ? Time.parse(amount) : amount
|
445
|
+
max_age = time - Time.now
|
446
|
+
end
|
447
|
+
|
448
|
+
values.last.merge!(:max_age => max_age) unless values.last[:max_age]
|
449
|
+
cache_control(*values)
|
450
|
+
|
451
|
+
@response['Expires'] = time.httpdate
|
452
|
+
end
|
453
|
+
|
454
|
+
|
455
|
+
##
|
456
|
+
# Sets Cache-Control, Expires, and Pragma headers to tell the browser
|
457
|
+
# not to cache the response.
|
458
|
+
|
459
|
+
def expire_cache_control
|
460
|
+
@response['Pragma'] = 'no-cache'
|
461
|
+
expires Time.new("1990","01","01"),
|
462
|
+
:no_cache, :no_store, :must_revalidate, max_age: 0
|
463
|
+
end
|
464
|
+
|
465
|
+
|
466
|
+
##
|
467
|
+
# Returns the url to an asset, including predefined asset cdn hosts if set.
|
468
|
+
|
469
|
+
def asset_url name
|
470
|
+
url = File.join(@app.asset_host_for(name).to_s, name)
|
471
|
+
url = [url, *@app.asset_version(url)].join("?") if url !~ %r{^https?://}
|
472
|
+
url
|
473
|
+
end
|
474
|
+
|
475
|
+
|
476
|
+
##
|
477
|
+
# Check if an asset exists.
|
478
|
+
# Returns the full system path to the asset if found, otherwise nil.
|
479
|
+
|
480
|
+
def asset path
|
481
|
+
@app.asset path
|
482
|
+
end
|
483
|
+
|
484
|
+
|
485
|
+
##
|
486
|
+
# Taken from Sinatra.
|
487
|
+
#
|
488
|
+
# Run the block with 'throw :halt' support and apply result to the response.
|
489
|
+
|
490
|
+
def invoke
|
491
|
+
res = catch(:halt) { yield }
|
492
|
+
res = [res] if Fixnum === res || String === res
|
493
|
+
if Array === res && Fixnum === res.first
|
494
|
+
res = res.dup
|
495
|
+
status(res.shift)
|
496
|
+
body(res.pop)
|
497
|
+
headers(*res)
|
498
|
+
elsif res.respond_to? :each
|
499
|
+
body res
|
500
|
+
end
|
501
|
+
nil # avoid double setting the same response tuple twice
|
502
|
+
end
|
503
|
+
|
504
|
+
|
505
|
+
##
|
506
|
+
# Dispatch the call to the action, calling before and after filers, and
|
507
|
+
# including error handling.
|
508
|
+
|
509
|
+
def dispatch action
|
510
|
+
@action = action
|
511
|
+
|
512
|
+
invoke do
|
513
|
+
filter(*before_filters_for(action))
|
514
|
+
args = action_arguments action
|
515
|
+
__send__(action, *args)
|
516
|
+
end
|
517
|
+
|
518
|
+
rescue => err
|
519
|
+
invoke{ handle_error err }
|
520
|
+
ensure
|
521
|
+
filter(*after_filters_for(action))
|
522
|
+
end
|
523
|
+
|
524
|
+
|
525
|
+
##
|
526
|
+
# In development mode, returns an HTML page displaying the full error
|
527
|
+
# and backtrace, otherwise shows a generic error page.
|
528
|
+
#
|
529
|
+
# Production error pages are first looked for in the public directory as
|
530
|
+
# <status>.html or 500.html. If none is found, falls back on Gin's internal
|
531
|
+
# error html pages.
|
532
|
+
|
533
|
+
def html_error_page err, code=nil
|
534
|
+
if @app.development?
|
535
|
+
fulltrace = err.backtrace.join("\n")
|
536
|
+
fulltrace = "<pre>#{fulltrace}</pre>"
|
537
|
+
|
538
|
+
apptrace = Gin.app_trace(err.backtrace).join("\n")
|
539
|
+
apptrace = "<pre>#{apptrace}</pre>" unless apptrace.empty?
|
540
|
+
|
541
|
+
DEV_ERROR_HTML % [err.class, err.class, err.message, apptrace, fulltrace]
|
542
|
+
|
543
|
+
else
|
544
|
+
code ||= status
|
545
|
+
filepath = asset("#{code}.html") || asset("500.html")
|
546
|
+
|
547
|
+
unless filepath
|
548
|
+
filepath = File.join(Gin::PUBLIC_DIR, "#{code}.html")
|
549
|
+
filepath = File.join(Gin::PUBLIC_DIR, "500.html") if !File.file?(filepath)
|
550
|
+
end
|
551
|
+
|
552
|
+
File.open(filepath, "rb")
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
|
557
|
+
private
|
558
|
+
|
559
|
+
|
560
|
+
DEV_ERROR_HTML = File.read(File.join(Gin::PUBLIC_DIR, "error.html")) #:nodoc:
|
561
|
+
|
562
|
+
BAD_REQ_MSG = "Expected param `%s'" #:nodoc:
|
563
|
+
|
564
|
+
##
|
565
|
+
# Get action arguments from the params.
|
566
|
+
# Raises Gin::BadRequest if a required argument has no matching param.
|
567
|
+
|
568
|
+
def action_arguments action=@action
|
569
|
+
raise Gin::NotFound, "No action #{self.class}##{action}" unless
|
570
|
+
self.class.actions.include? action.to_sym
|
571
|
+
|
572
|
+
args = []
|
573
|
+
temp = []
|
574
|
+
prev_type = nil
|
575
|
+
|
576
|
+
method(action).parameters.each do |(type, name)|
|
577
|
+
val = params[name.to_s]
|
578
|
+
|
579
|
+
raise Gin::BadRequest, BAD_REQ_MSG % name if type == :req && !val
|
580
|
+
break if type == :rest || type == :block || name.nil?
|
581
|
+
|
582
|
+
if type == :key
|
583
|
+
# Ruby 2.0 hash keys arguments
|
584
|
+
args.concat temp
|
585
|
+
args << {} if prev_type != :key
|
586
|
+
args.last[name] = val unless val.nil?
|
587
|
+
|
588
|
+
elsif val.nil?
|
589
|
+
temp << val
|
590
|
+
|
591
|
+
else
|
592
|
+
args.concat temp
|
593
|
+
temp.clear
|
594
|
+
args << val
|
595
|
+
end
|
596
|
+
|
597
|
+
prev_type = type
|
598
|
+
end
|
599
|
+
|
600
|
+
args
|
601
|
+
end
|
602
|
+
end
|