gin 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -41,6 +41,8 @@ class Gin::Config
41
41
 
42
42
  attr_accessor :dir, :logger, :ttl, :environment
43
43
 
44
+ SYNTAX_ERROR = defined?(Psych) ? Psych::SyntaxError : ArgumentError
45
+
44
46
  ##
45
47
  # Create a new config instance for the given environment name.
46
48
  # The environment dictates which part of the config files gets exposed.
@@ -120,7 +122,7 @@ class Gin::Config
120
122
  @data[name] = c
121
123
  end
122
124
 
123
- rescue Psych::SyntaxError
125
+ rescue SYNTAX_ERROR
124
126
  @logger.write "[ERROR] Could not parse config `#{filepath}' as YAML"
125
127
  return nil
126
128
  end
@@ -7,6 +7,8 @@ module Gin::Constants
7
7
  REMOTE_ADDR = 'REMOTE_ADDR'.freeze
8
8
  REMOTE_USER = 'REMOTE_USER'.freeze
9
9
  HTTP_VERSION = 'HTTP_VERSION'.freeze
10
+ SERVER_NAME = 'SERVER_NAME'.freeze
11
+ SERVER_PORT = 'SERVER_PORT'.freeze
10
12
  REQ_METHOD = 'REQUEST_METHOD'.freeze
11
13
  PATH_INFO = 'PATH_INFO'.freeze
12
14
  QUERY_STRING = 'QUERY_STRING'.freeze
@@ -14,6 +16,7 @@ module Gin::Constants
14
16
  IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
15
17
  IF_MOD_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
16
18
  IF_UNMOD_SINCE = 'HTTP_IF_UNMODIFIED_SINCE'.freeze
19
+ RACK_INPUT = 'rack.input'.freeze
17
20
 
18
21
  ASYNC_CALLBACK = 'async.callback'.freeze
19
22
 
@@ -27,13 +30,15 @@ module Gin::Constants
27
30
  CACHE_CTRL = 'Cache-Control'.freeze
28
31
  EXPIRES = 'Expires'.freeze
29
32
  PRAGMA = 'Pragma'.freeze
33
+ HOST_NAME = 'Host'.freeze
30
34
 
31
35
  # Gin env constants
36
+ GIN_APP = 'gin.app'.freeze
32
37
  GIN_STACK = 'gin.stack'.freeze
33
38
  GIN_ROUTE = 'gin.http_route'.freeze
34
39
  GIN_PATH_PARAMS = 'gin.path_query_hash'.freeze
40
+ GIN_TARGET = 'gin.target'.freeze
35
41
  GIN_CTRL = 'gin.controller'.freeze
36
- GIN_ACTION = 'gin.action'.freeze
37
42
  GIN_STATIC = 'gin.static'.freeze
38
43
  GIN_RELOADED = 'gin.reloaded'.freeze
39
44
  GIN_ERRORS = 'gin.errors'.freeze
@@ -49,6 +49,7 @@
49
49
 
50
50
  class Gin::Controller
51
51
  extend GinClass
52
+ extend Gin::Mountable
52
53
  include Gin::Constants
53
54
  include Gin::Filterable
54
55
  include Gin::Errorable
@@ -84,7 +85,7 @@ class Gin::Controller
84
85
  # Array of action names for this controller.
85
86
 
86
87
  def self.actions
87
- instance_methods(false)
88
+ instance_methods(false).map{|a| a.to_sym }
88
89
  end
89
90
 
90
91
 
@@ -93,6 +94,9 @@ class Gin::Controller
93
94
  # Underscores the class name and removes mentions of 'controller'.
94
95
  # MyApp::FooController.controller_name
95
96
  # #=> "my_app/foo"
97
+ #
98
+ # Note that when route names get autogenerated, the namespacing is dropped.
99
+ # If two routes have the same name, the last one defined wins.
96
100
 
97
101
  def self.controller_name new_name=nil
98
102
  @ctrl_name = new_name if new_name
@@ -100,6 +104,32 @@ class Gin::Controller
100
104
  end
101
105
 
102
106
 
107
+ DEFAULT_ACTION_MAP = {
108
+ :index => %w{GET /},
109
+ :show => %w{GET /:id},
110
+ :new => %w{GET /new},
111
+ :create => %w{POST /},
112
+ :edit => %w{GET /:id/edit},
113
+ :update => %w{PUT /:id},
114
+ :destroy => %w{DELETE /:id}
115
+ } # :nodoc:
116
+
117
+
118
+ def self.default_route_for action #:nodoc:
119
+ DEFAULT_ACTION_MAP[action] || ['GET', "/#{action}"]
120
+ end
121
+
122
+
123
+ def self.route_name_for action #:nodoc:
124
+ "#{action}_#{controller_name.sub(%r{^.*/},'')}".to_sym
125
+ end
126
+
127
+
128
+ def self.display_name action=nil #:nodoc:
129
+ [self, action].compact.join("#")
130
+ end
131
+
132
+
103
133
  ##
104
134
  # Set or get the default content type for this Gin::Controller.
105
135
  # Default value is "text/html". This attribute is inherited.
@@ -123,6 +153,18 @@ class Gin::Controller
123
153
  end
124
154
 
125
155
 
156
+ ##
157
+ # Call the Controller with an Rack env hash. Requires the hash to have
158
+ # the keys 'gin.target' with the action name as the second item of the array,
159
+ # and 'gin.app'.
160
+
161
+ def self.call env
162
+ inst = new(env[GIN_APP], env)
163
+ env[GIN_CTRL] = inst
164
+ inst.call_action(env[GIN_TARGET][1])
165
+ end
166
+
167
+
126
168
  ##
127
169
  # Get or set a layout for a given controller.
128
170
  # Value can be a symbol or filepath.
@@ -164,7 +206,11 @@ class Gin::Controller
164
206
  end
165
207
 
166
208
 
167
- def call_action action #:nodoc:
209
+ ##
210
+ # Calls the given or preset action and returns a Rack response Array.
211
+
212
+ def call_action action=nil
213
+ action ||= @action
168
214
  invoke{ dispatch action }
169
215
  invoke{ handle_status(@response.status) }
170
216
  content_type self.class.content_type unless @response[CNT_TYPE]
@@ -301,8 +347,6 @@ class Gin::Controller
301
347
  end
302
348
 
303
349
 
304
-
305
-
306
350
  ##
307
351
  # Set multiple response headers with Hash.
308
352
 
@@ -404,7 +448,8 @@ class Gin::Controller
404
448
 
405
449
  def path_to *args
406
450
  return "#{args[0]}#{"?" << Gin.build_query(args[1]) if args[1]}" if String === args[0]
407
- args.unshift(self.class) if Symbol === args[0] && respond_to?(args[0])
451
+ args.unshift(self.class) if Symbol === args[0] &&
452
+ self.class.actions.include?(args[0])
408
453
  @app.router.path_to(*args)
409
454
  end
410
455
 
@@ -471,6 +516,95 @@ class Gin::Controller
471
516
  end
472
517
 
473
518
 
519
+ ##
520
+ # Halt execution of the current controller action and create a new request to
521
+ # another action and/or controller, or path.
522
+ #
523
+ # Raises Gin::RouterError if the controller and action don't have a route.
524
+ # Returns a 404 response if an unrecognized path is given.
525
+ #
526
+ # The rewrite method acts just as if a request had been sent from the client,
527
+ # and will re-run any of the in-app middleware. If the app is itself running
528
+ # as middleware, you may use rewrite to pass a request down to the next item
529
+ # in the stack by specifying a path not supported by the app.
530
+ #
531
+ # Supports the same arguments at the Gin::Controller#url_to method.
532
+ #
533
+ # rewrite MyController, :action
534
+ # #=> Calls app with route for MyController#action
535
+ #
536
+ # rewrite MyController, :show, :id => 123
537
+ # #=> Calls app with route for MyController#action with the given params
538
+ #
539
+ # rewrite :show_foo
540
+ # #=> Calls app with route for the current controller's :show_foo action,
541
+ # #=> or if missing the controller and action for the :show_foo named route.
542
+ #
543
+ # # Rewrite and execute the request with the given headers.
544
+ # rewrite :show_foo, 'HTTP_X_CUSTOM_HEADER' => 'foo'
545
+ # rewrite :show_foo, params, 'HTTP_X_CUSTOM_HEADER' => 'foo'
546
+ #
547
+ # # Rewrite to an arbitrary path.
548
+ # rewrite '/path/to/something/else', {}, 'REQUEST_METHOD' => 'POST'
549
+ #
550
+ # Note that params are not forwarded with the rewrite call. The app
551
+ # considers this to be a completely different request, which means all params
552
+ # required must be passed explicitely.
553
+ #
554
+ # Streamed and IO request content is also ignored unless it is explicitely
555
+ # assigned to the 'rack.input' (as a part of the headers argument).
556
+
557
+ def rewrite *args
558
+ args.unshift(self.class) if Symbol === args[0] &&
559
+ self.class.actions.include?(args[0])
560
+ halt(*@app.rewrite!(@env, *args))
561
+ end
562
+
563
+
564
+ ##
565
+ # Unlike Gin::Controller#rewrite, the reroute method forwards the current
566
+ # request and params to the provided controller and/or action, or named route.
567
+ # Halts further execution in the current action.
568
+ # Raises RouterError if a given named route isn't found in the app's routes.
569
+ #
570
+ # reroute MyController, :action
571
+ # #=> Executes MyController#action
572
+ #
573
+ # reroute MyController, :show, :id => 123
574
+ # #=> Executes MyController#action with the given params merged to
575
+ # #=> the current params.
576
+ #
577
+ # reroute :show_foo
578
+ # #=> Executes the current controller's :show_foo action, or if missing
579
+ # #=> the controller and action for the :show_foo named route.
580
+ #
581
+ # # Reroute with the given headers.
582
+ # reroute :show_foo, {}, 'HTTP_X_CUSTOM_HEADER' => 'foo'
583
+
584
+ def reroute *args
585
+ args.unshift(self.class) if Symbol === args[0] &&
586
+ self.class.actions.include?(args[0])
587
+ nheaders = args.pop if Hash === args.last && Hash === args[-2] && args[-2] != args[-1]
588
+ nparams = args.pop if Hash === args.last
589
+
590
+ if Class === args[0]
591
+ ctrl_klass, naction = args[0..1]
592
+ else
593
+ route = @app.router.route_to(*args)
594
+ ctrl_klass, naction = route.target
595
+ end
596
+
597
+ nenv = @env.merge(nheaders || {})
598
+ nenv[GIN_PATH_PARAMS] = {}
599
+ ctrl = ctrl_klass.new(@app, nenv)
600
+
601
+ ctrl.params.merge!(params)
602
+ ctrl.params.merge!(nparams) if nparams
603
+
604
+ halt(*ctrl.call_action(naction))
605
+ end
606
+
607
+
474
608
  ##
475
609
  # Assigns a file to the response body and halts the execution of the action.
476
610
  # Produces a 404 response if no file is found.
@@ -510,9 +644,15 @@ class Gin::Controller
510
644
  def last_modified time
511
645
  return unless time
512
646
 
513
- time = Time.at(time) if Integer === time
514
- time = Time.parse(time) if String === time
515
- time = time.to_time if time.respond_to?(:to_time)
647
+ time = if Integer === time
648
+ Time.at(time)
649
+ elsif time.respond_to?(:to_time)
650
+ time.to_time
651
+ elsif !time.is_a?(Time)
652
+ Time.parse time.to_s
653
+ else
654
+ time
655
+ end
516
656
 
517
657
  @response[LAST_MOD] = time.httpdate
518
658
  return if @env[IF_NONE_MATCH]
@@ -596,7 +736,7 @@ class Gin::Controller
596
736
 
597
737
  def expire_cache_control
598
738
  @response[PRAGMA] = 'no-cache'
599
- expires EPOCH, :no_cache, :no_store, :must_revalidate, max_age: 0
739
+ expires EPOCH, :no_cache, :no_store, :must_revalidate, :max_age => 0
600
740
  end
601
741
 
602
742
 
@@ -604,9 +744,28 @@ class Gin::Controller
604
744
  # Returns the url to an asset, including predefined asset cdn hosts if set.
605
745
 
606
746
  def asset_url name
607
- url = File.join(@app.asset_host_for(name).to_s, name)
608
- url = [url, *@app.asset_version(url)].join("?") if url !~ %r{^https?://}
609
- url
747
+ host = @app.asset_host_for(name)
748
+ return asset_path(name) if !host
749
+ File.join(host, name)
750
+ end
751
+
752
+
753
+ ##
754
+ # Returns the HTTP path to the local asset.
755
+
756
+ def asset_path name
757
+ fdpath = @app.asset(name)
758
+
759
+ if fdpath && fdpath.start_with?(@app.assets_dir)
760
+ if fdpath.start_with?(@app.public_dir)
761
+ fdpath[@app.public_dir.length..-1]
762
+ else
763
+ fdpath[@app.assets_dir.length..-1]
764
+ end
765
+ else
766
+ path = File.join('', name)
767
+ [path, *@app.asset_version(name)].compact.join("?")
768
+ end
610
769
  end
611
770
 
612
771
 
@@ -643,7 +802,7 @@ class Gin::Controller
643
802
  # #=> "<root_dir>/other/foo"
644
803
 
645
804
  def template_path template, is_layout=false
646
- dir = if template[0] == ?/
805
+ dir = if template.to_s[0] == ?/
647
806
  @app.root_dir
648
807
  elsif is_layout
649
808
  @app.layouts_dir
@@ -765,10 +924,11 @@ class Gin::Controller
765
924
 
766
925
  def html_error_page err, code=nil
767
926
  if @app.development?
768
- fulltrace = err.backtrace.join("\n")
927
+ backtrace = err.backtrace || ['No backtrace :(']
928
+ fulltrace = backtrace.join("\n")
769
929
  fulltrace = "<pre>#{h(fulltrace)}</pre>"
770
930
 
771
- apptrace = Gin.app_trace(err.backtrace).join("\n")
931
+ apptrace = Gin.app_trace(backtrace).join("\n")
772
932
  apptrace = "<pre>#{h(apptrace)}</pre>" unless apptrace.empty?
773
933
 
774
934
  DEV_ERROR_HTML %
@@ -807,9 +967,12 @@ class Gin::Controller
807
967
  # Get action arguments from the params.
808
968
  # Raises Gin::BadRequest if a required argument has no matching param.
809
969
 
810
- def action_arguments action=@action
970
+ def action_arguments action
971
+ raise Gin::NotFound,
972
+ "No action exists for: #{env[REQ_METHOD]} #{env[PATH_INFO]}" unless action
973
+
811
974
  raise Gin::NotFound, "No action #{self.class}##{action}" unless
812
- self.class.actions.include? action.to_sym
975
+ self.class.actions.include?(action.to_sym)
813
976
 
814
977
  args = []
815
978
  temp = []
@@ -0,0 +1,10 @@
1
+ if Float.instance_method(:round).arity == 0
2
+ class Float
3
+ undef round
4
+ def round ndigits=0
5
+ num, dec = self.to_s.split(".")
6
+ num = "#{num}.#{dec[0,ndigits]}".sub(/\.$/, "")
7
+ Float num
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,41 @@
1
+ unless Time.now.respond_to?(:httpdate)
2
+
3
+ class Time
4
+ def httpdate(date)
5
+ if /\A\s*
6
+ (?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\x20
7
+ (\d{2})\x20
8
+ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\x20
9
+ (\d{4})\x20
10
+ (\d{2}):(\d{2}):(\d{2})\x20
11
+ GMT
12
+ \s*\z/x =~ date
13
+ self.rfc2822(date)
14
+ elsif /\A\s*
15
+ (?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday),\x20
16
+ (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d)\x20
17
+ (\d\d):(\d\d):(\d\d)\x20
18
+ GMT
19
+ \s*\z/x =~ date
20
+ year = $3.to_i
21
+ if year < 50
22
+ year += 2000
23
+ else
24
+ year += 1900
25
+ end
26
+ self.utc(year, $2, $1.to_i, $4.to_i, $5.to_i, $6.to_i)
27
+ elsif /\A\s*
28
+ (?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\x20
29
+ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\x20
30
+ (\d\d|\x20\d)\x20
31
+ (\d\d):(\d\d):(\d\d)\x20
32
+ (\d{4})
33
+ \s*\z/x =~ date
34
+ self.utc($6.to_i, MonthValue[$1.upcase], $2.to_i,
35
+ $3.to_i, $4.to_i, $5.to_i)
36
+ else
37
+ raise ArgumentError.new("not RFC 2616 compliant date: #{date.inspect}")
38
+ end
39
+ end
40
+ end
41
+ end
@@ -74,14 +74,14 @@ module Gin::Filterable
74
74
 
75
75
  elsif opts[:except]
76
76
  except = Array(opts[:except])
77
- filter_hsh.keys.each do |action|
78
- next if action.nil?
79
- filter_hsh[action] ||= filter_hsh[nil].dup and next if
80
- except.include?(action)
77
+ except.each do |action|
78
+ filter_hsh[action] ||= filter_hsh[nil].dup
79
+ end
81
80
 
81
+ filter_hsh.keys.each do |action|
82
+ next if except.include?(action)
82
83
  yield filter_hsh, action, names
83
84
  end
84
- yield filter_hsh, nil, names
85
85
 
86
86
  else
87
87
  filter_hsh.keys.each do |action|
@@ -0,0 +1,100 @@
1
+ ##
2
+ # The Gin::Mountable module provides an interface to mount any type of object
3
+ # to a Gin::App route.
4
+ #
5
+ # The Gin::Mountable module is only necessary if features such automatic route
6
+ # naming and defaults are needed. Gin will otherwise happily mount any object
7
+ # that responds to the `call' method onto it's routing tree.
8
+
9
+ module Gin::Mountable
10
+
11
+ def verify_mount! #:nodoc:
12
+ controller_name.to_str
13
+
14
+ test_action = actions[0]
15
+
16
+ test_route = default_route_for(test_action)
17
+ test_route[0].to_str
18
+ test_route[1].to_str
19
+
20
+ route_name_for(test_action).to_proc
21
+
22
+ display_name.to_str
23
+ display_name(test_action).to_str
24
+
25
+ true
26
+ end
27
+
28
+
29
+ ##
30
+ # The actions available on this Mountable object.
31
+ # Actions can be of any object type.
32
+ #
33
+ # Must return an Array of actions. Must be overloaded.
34
+ # UserController.actions
35
+ # #=> [:show, :delete, :list, :update]
36
+
37
+ def actions
38
+ raise NoMethodError,
39
+ "The `#{__method__}' method must be defined on #{self} and return an\
40
+ Array of available actions for the Router to map to."
41
+ end
42
+
43
+
44
+ ##
45
+ # The String representing the controller.
46
+ #
47
+ # Must return a String. Must be overloaded.
48
+ # UserController.controller_name
49
+ # #=> 'user'
50
+
51
+ def controller_name
52
+ raise NoMethodError,
53
+ "The `#{__method__}' method must be defined on #{self} and return a\
54
+ String representing the controller name."
55
+ end
56
+
57
+
58
+ ##
59
+ # Should return a 2 item Array with the HTTP verb and request path (local to
60
+ # the controller) to use for a given action.
61
+ #
62
+ # Must return a 2 item Array of Strings. Must be overloaded.
63
+ # UserController.default_route_for :show
64
+ # #=> ['GET', '/:id']
65
+
66
+ def default_route_for action
67
+ raise NoMethodError,
68
+ "The `#{__method__}' method must be defined on #{self} and return a\
69
+ 2 item Array with the HTTP verb and local path: ['GET', '/users/:id']"
70
+ end
71
+
72
+
73
+ ##
74
+ # Creates a route name used to identify a given route. Used by helper methods.
75
+ #
76
+ # Must return a Symbol, or nil. Must be overloaded.
77
+ # UserController.route_name_for :show
78
+ # #=> :show_user
79
+
80
+ def route_name_for action
81
+ raise NoMethodError,
82
+ "The `#{__method__}' method must be defined on #{self} and return a\
83
+ Symbol representing the route name: :show_user"
84
+ end
85
+
86
+
87
+ ##
88
+ # Creates a display name for the Controller (and optional action).
89
+ # Used for logging and error messages.
90
+ #
91
+ # Must return a String. Must be overloaded.
92
+ # UserController.display_name :show
93
+ # #=> "UserController#show"
94
+
95
+ def display_name action=nil
96
+ raise NoMethodError,
97
+ "The `#{__method__}' method must be defined on #{self} and return a\
98
+ String for display purposes"
99
+ end
100
+ end