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