gin 1.1.2 → 1.2.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.
@@ -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