gin 0.0.0 → 1.0.0

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