rocketio 0.1.0 → 0.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rocketio.rb +6 -3
  3. data/lib/rocketio/application.rb +3 -7
  4. data/lib/rocketio/controller.rb +62 -90
  5. data/lib/rocketio/controller/authentication.rb +38 -44
  6. data/lib/rocketio/controller/authorization.rb +8 -4
  7. data/lib/rocketio/controller/error_handlers.rb +12 -8
  8. data/lib/rocketio/controller/filters.rb +14 -19
  9. data/lib/rocketio/controller/helpers.rb +1 -1
  10. data/lib/rocketio/controller/middleware.rb +1 -1
  11. data/lib/rocketio/controller/render/engine.rb +3 -3
  12. data/lib/rocketio/controller/render/layout.rb +1 -1
  13. data/lib/rocketio/controller/render/layouts.rb +6 -6
  14. data/lib/rocketio/controller/render/template_vars.rb +3 -3
  15. data/lib/rocketio/controller/render/templates.rb +6 -6
  16. data/lib/rocketio/controller/sessions.rb +1 -1
  17. data/lib/rocketio/error_templates/409.html +11 -7
  18. data/lib/rocketio/error_templates/501.html +4 -4
  19. data/lib/rocketio/router.rb +35 -21
  20. data/lib/rocketio/version.rb +1 -1
  21. data/rocketio.gemspec +2 -0
  22. data/test/aliases_test.rb +2 -2
  23. data/test/api_test.rb +24 -117
  24. data/test/authentication_test.rb +96 -60
  25. data/test/authorization_test.rb +28 -17
  26. data/test/cache_control_test.rb +12 -12
  27. data/test/content_type_test.rb +7 -7
  28. data/test/cookies_test.rb +4 -4
  29. data/test/error_handlers_test.rb +14 -12
  30. data/test/etag_test.rb +32 -32
  31. data/test/filters_test.rb +96 -79
  32. data/test/halt_test.rb +1 -1
  33. data/test/helpers_test.rb +6 -6
  34. data/test/middleware_test.rb +4 -4
  35. data/test/redirect_test.rb +6 -7
  36. data/test/render/{post.erb → b.erb} +0 -0
  37. data/test/render/{put.erb → c.erb} +0 -0
  38. data/test/render/engine_test.rb +5 -5
  39. data/test/render/{get.erb → index.erb} +0 -0
  40. data/test/render/layout_test.rb +21 -17
  41. data/test/render/layouts_test.rb +14 -14
  42. data/test/render/render_test.rb +17 -14
  43. data/test/render/template_vars_test.rb +9 -9
  44. data/test/render/templates_test.rb +16 -16
  45. data/test/response_test.rb +4 -4
  46. data/test/routes_test.rb +21 -42
  47. data/test/sendfile_test.rb +8 -8
  48. data/test/sessions_test.rb +27 -27
  49. data/test/setup.rb +2 -0
  50. metadata +34 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e9540db8ccba375983a890602be457287f31f7e2
4
- data.tar.gz: aeeba39bb33341e1c015c099040793df15e1b396
3
+ metadata.gz: c81e2dd778eb2ae80019ccfdbd0a07da71685609
4
+ data.tar.gz: 18fb36dc539e788db0c8ca0a4143fbf8867eab23
5
5
  SHA512:
6
- metadata.gz: 135b3a8e35301383adf5b21ae1a3a80aa0639feb1d44d5cd96e82c4c4da453844890cffe4517aceb2b5642d664d9893bd8889c10d7d05a58026a4e7220c1544b
7
- data.tar.gz: b526fcf90b86d59f0e1a3cf6cd847c1d752919ef4e27a6c9b4e85a741b06795e1a3b7ed2554dfbfe300d3e5b3014390e224840939008a00b2ebf155d37c31873
6
+ metadata.gz: b5d4954032b4b5fedfa2d43cfff1b923f8a43fabab4eb41bf3e3fa5592ad7459fc836859553b2e37d84ec65812f04a4371ab7a276ddcbdde09892c2aa88c9ba4
7
+ data.tar.gz: 5d19a34c16c3f4aaf635940796a6f1d435d11d4a12de9c6e55b5dc543c830ad79d83a2db26b9e47e206cbf4db3a23b61a14999e285aa2dce8388b44955f09f24
data/lib/rocketio.rb CHANGED
@@ -47,13 +47,16 @@ module RocketIO
47
47
  HEAD => :head
48
48
  }.freeze
49
49
 
50
+ INDEX_METHOD = :index
51
+
50
52
  EMPTY_STRING = ''.freeze
51
53
  EMPTY_STRING_PROC = proc {RocketIO::EMPTY_STRING}
52
54
  EMPTY_ARRAY = [].freeze
53
55
  EMPTY_HASH = {}.freeze
54
56
 
55
- SLASH = '/'.freeze
56
- QUERY_PREFIX = '?'.freeze
57
+ SLASH = '/'.freeze
58
+ QUERY_PREFIX = '?'.freeze
59
+ PATH_SPLITTER = /[^\/]+/.freeze
57
60
 
58
61
  CONTENT_TYPE = 'Content-Type'.freeze
59
62
  DEFAULT_CONTENT_TYPE = 'text/html'.freeze
@@ -72,7 +75,7 @@ module RocketIO
72
75
  HTTP_AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION'].map(&:freeze).freeze
73
76
  HTTP_AUTHORIZATION_MOCKS = {
74
77
  basic: 'Basic Og=='.freeze,
75
- digest: 'Digest opaque="", qop="auth", uri="/"'.freeze
78
+ digest: 'Digest opaque="", qop="auth", uri="%s"'.freeze
76
79
  }.freeze
77
80
  HTTP_UPGRADE = 'HTTP_UPGRADE'.freeze
78
81
  UPGRADE = 'upgrade'.freeze
@@ -11,15 +11,11 @@ module RocketIO
11
11
 
12
12
  def call env
13
13
  catch :__response__ do # catch 404 errors and potential `halt` calls from middleware
14
- controller, path_params = @router.resolve_path(env[PATH_INFO])
14
+ controller, method, path_params = @router.resolve_path(env[PATH_INFO])
15
15
 
16
- unless controller
17
- controller = RocketIO::Controller.initialize_controller
18
- controller.env = env
19
- controller.error(404)
20
- end
16
+ controller || Controller.new(INDEX_METHOD, EMPTY_ARRAY, env).error(404)
21
17
 
22
- controller = controller.initialize_controller(REQUEST_METHODS[env[REQUEST_METHOD]], path_params)
18
+ controller = controller.new(method, path_params)
23
19
  chain = controller.middleware.reverse.inject(controller) {|app,ware| ware.call(app)}
24
20
  if controller.sessions
25
21
  chain = controller.sessions[0].new(chain, controller.sessions[1])
@@ -2,35 +2,36 @@ module RocketIO
2
2
  class Controller
3
3
  extend Forwardable
4
4
 
5
- def_delegators 'self.class', :url, :dirname, :parameters_policy
5
+ def_delegators :'self.class', :url, :api, :dirname, :parameters_policy
6
6
  def_delegators :request, :session, :request_method
7
- def_delegators RocketIO, :indifferent_params, :indifferent_hash, :mime_type, :engine_const, :engine_class
7
+ def_delegators RocketIO, :environment
8
+ def_delegators RocketIO, :indifferent_params, :indifferent_hash, :mime_type
9
+ def_delegators RocketIO, :engine_const, :engine_class
8
10
  def_delegators CGI, :escape_html
9
11
 
10
- def_delegators RocketIO, :environment
12
+ # defining instance helpers for querying current environment like
13
+ # development? production? etc.
11
14
  RocketIO::ENVIRONMENTS.each_key do |env|
12
15
  def_delegators RocketIO, :"#{env}?"
13
16
  end
14
17
 
15
- # defining get, post etc. methods that will be called
16
- # when a request matches current controller and appropriate request method used.
17
- #
18
- # by default all requests, except HEAD, will return a NotImplementedError.
19
- # override the methods you need to be handled by controller.
20
- #
21
- RocketIO::REQUEST_METHODS.each_value do |verb|
22
- define_method(verb) {|*| error(501)}
18
+ def initialize requested_method = RocketIO::INDEX_METHOD, path_params = RocketIO::EMPTY_ARRAY, env = nil
19
+ @__requested_method__ = requested_method
20
+ @__path_params_array__ = path_params.freeze
21
+ @__env__ = env if env
23
22
  end
24
- def head(*); end
25
23
 
26
- # call requested method.
27
- # also call #before, #around and #after filters.
24
+ # call requested method
28
25
  #
29
26
  # @param [Hash] env
30
27
  # @return [Rack::Response]
31
28
  #
32
29
  def call env
30
+ @__env__ = env
33
31
  catch :__response__ do
32
+
33
+ api[requested_method] || error(501)
34
+
34
35
  if error_handlers[500]
35
36
  begin
36
37
  __call__(env)
@@ -43,8 +44,9 @@ module RocketIO
43
44
  end
44
45
  end
45
46
 
46
- private def __call__ env
47
- self.env = env
47
+ private
48
+ def __call__ env
49
+
48
50
  validate_or_request_authentication_if_needed
49
51
  validate_or_request_authorization_if_needed
50
52
  validate_parameters
@@ -57,53 +59,51 @@ module RocketIO
57
59
  invoke_after_filter
58
60
  }
59
61
 
60
- response.body ||= RocketIO::EMPTY_ARRAY
61
- response.body = [] if head? # dropping body on HEAD requests
62
+ if head? # dropping body on HEAD requests
63
+ response.body = RocketIO::EMPTY_ARRAY
64
+ else
65
+ response.body ||= RocketIO::EMPTY_ARRAY
66
+ end
67
+
62
68
  response.finish
63
69
  end
64
70
 
65
- private def __run__ app
71
+ def __run__ app
66
72
  app.call
67
73
  end
68
74
 
69
- def env= env
70
- @__env__ = env
71
- end
72
-
73
75
  def env
74
76
  @__env__
75
77
  end
76
78
 
77
79
  def validate_parameters
78
- # enforce policy only if a method defined for current request method
79
- # cause stock REST methods accepting any number of arguments
80
- return unless policy = parameters_policy[requested_method]
81
80
 
82
- if path_params_array.size >= policy[:min]
83
- return if policy[:max] == :* || path_params_array.size <= policy[:max]
81
+ if path_params_array.size >= api[requested_method][:parameters_policy][:min]
82
+ return if api[requested_method][:parameters_policy][:max] == :* ||
83
+ path_params_array.size <= api[requested_method][:parameters_policy][:max]
84
84
  end
85
85
 
86
86
  error(409)
87
87
  end
88
88
 
89
- def path_params
90
- @__path_params__ ||= begin
91
- rangemap = self.class.path_params[requested_method] ||
92
- raise(StandardError, 'No path_params map found for %s method' % requested_method)
93
- indifferent_params(rangemap.each_with_object({}) {|(m,r),o|
94
- o[m] = if r.min && r.max
95
- r.min == r.max ? path_params_array[r.min] : path_params_array[r]
96
- else
97
- path_params_array[r]
98
- end
99
- }).freeze
100
- end
89
+ def requested_method
90
+ @__requested_method__
101
91
  end
102
92
 
103
93
  def path_params_array
104
94
  @__path_params_array__
105
95
  end
106
96
 
97
+ def path_params
98
+ @__path_params__ ||= api[requested_method][:path_params].each_with_object({}) {|(m,r),o|
99
+ o[m] = if r.min && r.max
100
+ r.min == r.max ? path_params_array[r.min] : path_params_array[r]
101
+ else
102
+ path_params_array[r]
103
+ end
104
+ }.freeze
105
+ end
106
+
107
107
  def params
108
108
  @__params__ ||= indifferent_params(request.params)
109
109
  end
@@ -115,30 +115,12 @@ module RocketIO
115
115
  def response
116
116
  @__response__ ||= RocketIO::Response.new
117
117
  end
118
-
119
- def requested_method
120
- @__requested_method__ ||= RocketIO::REQUEST_METHODS[request_method]
121
- end
122
118
  end
123
119
 
124
120
  class << Controller
125
121
 
126
- # only public non-inherited methods are included in public API directly.
127
122
  def api
128
- return [] if self == RocketIO::Controller
129
- public_instance_methods(false).concat(inherited_api) - private_api
130
- end
131
-
132
- # inherited methods are excluded from public API
133
- # but we still need a way to know what API methods was inherited
134
- def inherited_api
135
- @__inherited_api__
136
- end
137
-
138
- # used internally to keep a list of public methods
139
- # that should be excluded from public API
140
- def private_api
141
- @__private_api__
123
+ @__api__
142
124
  end
143
125
 
144
126
  # import some config from some controller
@@ -147,11 +129,10 @@ module RocketIO
147
129
  end
148
130
 
149
131
  def inherited base
150
- # registering new controller
132
+ # registering a new controller
151
133
  RocketIO.controllers.push(base)
152
134
 
153
- base.instance_variable_set(:@__private_api__, self.private_api.uniq)
154
- base.instance_variable_set(:@__inherited_api__, self.api.freeze)
135
+ base.instance_variable_set(:@__api__, self.api.dup)
155
136
 
156
137
  # new controller inherits all setups from superclass
157
138
  base.import :before, from: self
@@ -250,10 +231,11 @@ module RocketIO
250
231
  @__aliases__ ||= []
251
232
  end
252
233
 
253
- # build a URL from given chunks prefixing them with actual path
234
+ # build a URL from given chunks prefixing them with controller's baseurl
254
235
  #
255
236
  # @param *args [Array]
256
237
  # @return [String]
238
+ #
257
239
  def url *args
258
240
  return @__url__ if args.empty?
259
241
  query = if args.last.is_a?(Hash)
@@ -265,45 +247,35 @@ module RocketIO
265
247
  end
266
248
 
267
249
  def method_added meth
268
- parameters = instance_method(meth).parameters
269
- path_params[meth] = RocketIO.path_params(parameters).freeze
270
- if requested_method = RocketIO::REQUEST_METHODS.values.find {|verb| verb == meth}
271
- # REST methods should be called with a predetermined set of parameters.
272
- # setting an appropriate policy for just defined method based on its parameters.
273
- parameters_policy[requested_method] = RocketIO.parameters_policy(parameters).freeze
274
- end
275
- end
250
+ return if self == RocketIO::Controller
251
+ return unless public_instance_methods(false).include?(meth)
276
252
 
277
- # initializing the controller to process a HTTP request
278
- #
279
- # @param path_params [Array]
280
- # @return a RocketIO::Route instance
281
- def initialize_controller requested_method = nil, path_params_array = nil
282
- controller = allocate
283
- controller.instance_variable_set(:@__requested_method__, requested_method.to_sym) if requested_method
284
- controller.instance_variable_set(:@__path_params_array__, (path_params_array || []).freeze)
285
- controller
286
- end
287
-
288
- def parameters_policy
289
- @__parameters_policy__ ||= {}
290
- end
253
+ parameters = instance_method(meth).parameters
291
254
 
292
- def path_params
293
- @__path_params__ ||= {}
255
+ api[meth] = {
256
+ # api methods should be called with a predetermined set of parameters
257
+ # so setting an appropriate policy for just defined method based on its parameters.
258
+ path_params: RocketIO.path_params(parameters).freeze,
259
+ parameters_policy: RocketIO.parameters_policy(parameters).freeze
260
+ }
294
261
  end
295
262
 
296
263
  def dirname *args
297
264
  ::File.join(@__dirname__, args.map!(&:to_s))
298
265
  end
299
266
 
300
- # making controller to act as a Rack application
301
267
  def call env
302
- initialize_controller.call(env)
268
+ path_params = env[RocketIO::PATH_INFO].sub(url, RocketIO::EMPTY_STRING).scan(RocketIO::PATH_SPLITTER)
269
+ method = if path_params.any? && api[path_params[0].to_sym]
270
+ path_params.slice!(0).to_sym
271
+ else
272
+ RocketIO::INDEX_METHOD
273
+ end
274
+ new(method, path_params).call(env)
303
275
  end
304
276
  end
305
277
 
306
- Controller.instance_variable_set(:@__private_api__, [])
278
+ Controller.instance_variable_set(:@__api__, {})
307
279
  end
308
280
 
309
281
  require 'rocketio/controller/authentication'
@@ -3,37 +3,34 @@ module RocketIO
3
3
 
4
4
  # easily restrict access to controller using basic auth
5
5
  #
6
- # @example protect all request methods
6
+ # @example protect all methods on any request methods
7
7
  # basic_auth do |user,pass|
8
8
  # user == 'admin' && pass == 'super secret password'
9
9
  # end
10
10
  #
11
- # @example protect only POST, PUT and DELETE request methods
12
- # basic_auth :post, :put, :delete do |user,pass|
13
- # user == 'admin' && pass == 'super secret password'
11
+ # @example protect all methods only on POST, PUT and DELETE request methods
12
+ # basic_auth do |user,pass|
13
+ # post? || put? || delete? ?
14
+ # user == 'admin' && pass == 'super secret password' :
15
+ # true
14
16
  # end
15
17
  #
16
- # @example use different credentials for GET and POST
17
- # basic_auth :get do |user,pass|
18
+ # @example protect only :edit
19
+ # basic_auth :edit do |user,pass|
18
20
  # user == 'reader' && pass == 'readPass'
19
21
  # end
20
22
  #
21
- # basic_auth :post do |user,pass|
22
- # user == 'poster' && pass == 'writePass'
23
- # end
24
- #
25
- # @note authorization is composable, that's it, if superclass is protecting :get method
26
- # and current controller protects :post method,
27
- # both :get and :post will be protected in current controller
23
+ # @note authorization is composable, that's it, if superclass is protecting :a
24
+ # and current controller is protecting :b method,
25
+ # both :a and :b will be protected in current controller
28
26
  #
29
27
  # @params *args [Array]
30
28
  # @param block [Proc]
31
29
  #
32
30
  def self.basic_auth *args, &block
33
31
  opts = args.last.is_a?(Hash) ? args.pop : {}
34
- rqms = args.any? ? args.map!(&:to_sym) : RocketIO::REQUEST_METHODS.values
35
- rqms.each do |rm|
36
- (@__basic_auth__ ||= {})[rm] = {
32
+ (args.any? ? args.map(&:to_sym) : [:*]).each do |method|
33
+ (@__basic_auth__ ||= {})[method] = {
37
34
  class: Rack::Auth::Basic,
38
35
  arguments: [opts[:realm] || RocketIO::DEFAULT_AUTH_REALM].freeze,
39
36
  block: block,
@@ -44,20 +41,20 @@ module RocketIO
44
41
  end
45
42
 
46
43
  def self.define_basic_auth_methods source = self
47
- prompts = (source.instance_variable_get(:@__basic_auth__) || {}).each_with_object(allocate.basic_auth.dup) do |(rm,p),o|
48
- method = :"__basic_auth__#{rm}__"
49
- private_api << define_method(method, &p[:block])
50
- o[rm] = p.merge(method: method).freeze
44
+ prompts = (source.instance_variable_get(:@__basic_auth__) || {}).each_with_object(allocate.basic_auth.dup) do |(m,p),o|
45
+ method = :"__basic_auth__#{m}__"
46
+ api.delete define_method(method, &p[:block])
47
+ o[m] = p.merge(method: method).freeze
51
48
  end.freeze
52
49
  return if prompts.empty?
53
- private_api << define_method(:basic_auth) {prompts}
50
+ api.delete define_method(:basic_auth) {prompts}
54
51
  end
55
52
 
56
53
  def basic_auth; RocketIO::EMPTY_HASH end
57
54
 
58
55
  # easily restrict access to controller using digest auth
59
56
  #
60
- # @example protect all request methods using hashed passwords
57
+ # @example protect all methods using hashed passwords
61
58
  # # hash the password somewhere in irb:
62
59
  # # ::Digest::MD5.hexdigest 'admin:AccessRestricted:somePassword'
63
60
  # # username ^ realm ^ password ^
@@ -68,25 +65,16 @@ module RocketIO
68
65
  # {'admin' => '9d77d54decc22cdcfb670b7b79ee0ef0'}[user]
69
66
  # end
70
67
  #
71
- # @example protect all request methods using plain passwords
68
+ # @example protect all methods using plain passwords
72
69
  # digest_auth do |user|
73
70
  # {'admin' => 'password'}[user]
74
71
  # end
75
72
  #
76
- # @example protect only POST, PUT and DELETE request methods
77
- # digest_auth :post, :put, :delete do |user|
73
+ # @example protect only :create and :edit methods
74
+ # digest_auth :create, :edit do |user|
78
75
  # {'admin' => 'password'}[user]
79
76
  # end
80
77
  #
81
- # @example use different credentials for GET and POST
82
- # digest_auth :get do |user|
83
- # {'user' => 'readPass'}[user]
84
- # end
85
- #
86
- # digest_auth :post do |user|
87
- # {'poster' => 'writePass'}[user]
88
- # end
89
- #
90
78
  # @params *args [Array]
91
79
  # @param block [Proc]
92
80
  #
@@ -94,9 +82,8 @@ module RocketIO
94
82
  opts = args.last.is_a?(Hash) ? args.pop : {}
95
83
  opts[:realm] ||= RocketIO::DEFAULT_AUTH_REALM
96
84
  opts[:opaque] ||= opts[:realm]
97
- rqms = args.any? ? args.map!(&:to_sym) : RocketIO::REQUEST_METHODS.values
98
- rqms.each do |rm|
99
- (@__digest_auth__ ||= {})[rm] = {
85
+ (args.any? ? args.map(&:to_sym) : [:*]).each do |method|
86
+ (@__digest_auth__ ||= {})[method] = {
100
87
  class: Rack::Auth::Digest::MD5,
101
88
  arguments: [opts].freeze,
102
89
  block: block,
@@ -107,13 +94,13 @@ module RocketIO
107
94
  end
108
95
 
109
96
  def self.define_digest_auth_methods source = self
110
- prompts = (source.instance_variable_get(:@__digest_auth__) || {}).each_with_object(allocate.digest_auth.dup) do |(rm,p),o|
111
- method = :"__digest_auth__#{rm}__"
112
- private_api << define_method(method, &p[:block])
113
- o[rm] = p.merge(method: method).freeze
97
+ prompts = (source.instance_variable_get(:@__digest_auth__) || {}).each_with_object(allocate.digest_auth.dup) do |(m,p),o|
98
+ method = :"__digest_auth__#{m}__"
99
+ api.delete define_method(method, &p[:block])
100
+ o[m] = p.merge(method: method).freeze
114
101
  end.freeze
115
102
  return if prompts.empty?
116
- private_api << define_method(:digest_auth) {prompts}
103
+ api.delete define_method(:digest_auth) {prompts}
117
104
  end
118
105
 
119
106
  def digest_auth; RocketIO::EMPTY_HASH end
@@ -123,17 +110,24 @@ module RocketIO
123
110
  # checks whether authentication is required and
124
111
  # send an authorization request if credentials not present or invalid
125
112
  def validate_or_request_authentication_if_needed
126
- return unless auth = digest_auth[requested_method] || basic_auth[requested_method]
113
+ return unless auth = authentication_required?
127
114
  return unless prompt = auth[:class].new(proc {}, *auth[:arguments]) do |*a|
128
115
  self.__send__(auth[:method], *a)
129
116
  end.call(
130
117
  if RocketIO::HTTP_AUTHORIZATION_KEYS.detect {|key| env.has_key?(key)}
131
118
  env
132
119
  else
133
- env.merge(RocketIO::HTTP_AUTHORIZATION_KEYS.first => auth[:mock])
120
+ env.merge(RocketIO::HTTP_AUTHORIZATION_KEYS.first => auth[:mock] % env[RocketIO::PATH_INFO])
134
121
  end
135
122
  )
136
123
  throw(:__response__, prompt)
137
124
  end
125
+
126
+ def authentication_required?
127
+ digest_auth[requested_method] ||
128
+ basic_auth[requested_method] ||
129
+ digest_auth[:*] ||
130
+ basic_auth[:*]
131
+ end
138
132
  end
139
133
  end