roda 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +709 -0
  5. data/Rakefile +124 -0
  6. data/lib/roda.rb +608 -0
  7. data/lib/roda/plugins/all_verbs.rb +48 -0
  8. data/lib/roda/plugins/default_headers.rb +50 -0
  9. data/lib/roda/plugins/error_handler.rb +69 -0
  10. data/lib/roda/plugins/flash.rb +62 -0
  11. data/lib/roda/plugins/h.rb +24 -0
  12. data/lib/roda/plugins/halt.rb +79 -0
  13. data/lib/roda/plugins/header_matchers.rb +57 -0
  14. data/lib/roda/plugins/hooks.rb +106 -0
  15. data/lib/roda/plugins/indifferent_params.rb +47 -0
  16. data/lib/roda/plugins/middleware.rb +88 -0
  17. data/lib/roda/plugins/multi_route.rb +77 -0
  18. data/lib/roda/plugins/not_found.rb +62 -0
  19. data/lib/roda/plugins/pass.rb +34 -0
  20. data/lib/roda/plugins/render.rb +217 -0
  21. data/lib/roda/plugins/streaming.rb +165 -0
  22. data/spec/composition_spec.rb +19 -0
  23. data/spec/env_spec.rb +11 -0
  24. data/spec/integration_spec.rb +63 -0
  25. data/spec/matchers_spec.rb +658 -0
  26. data/spec/module_spec.rb +29 -0
  27. data/spec/opts_spec.rb +42 -0
  28. data/spec/plugin/all_verbs_spec.rb +29 -0
  29. data/spec/plugin/default_headers_spec.rb +63 -0
  30. data/spec/plugin/error_handler_spec.rb +67 -0
  31. data/spec/plugin/flash_spec.rb +59 -0
  32. data/spec/plugin/h_spec.rb +13 -0
  33. data/spec/plugin/halt_spec.rb +62 -0
  34. data/spec/plugin/header_matchers_spec.rb +61 -0
  35. data/spec/plugin/hooks_spec.rb +97 -0
  36. data/spec/plugin/indifferent_params_spec.rb +13 -0
  37. data/spec/plugin/middleware_spec.rb +52 -0
  38. data/spec/plugin/multi_route_spec.rb +98 -0
  39. data/spec/plugin/not_found_spec.rb +99 -0
  40. data/spec/plugin/pass_spec.rb +23 -0
  41. data/spec/plugin/render_spec.rb +148 -0
  42. data/spec/plugin/streaming_spec.rb +52 -0
  43. data/spec/plugin_spec.rb +61 -0
  44. data/spec/redirect_spec.rb +24 -0
  45. data/spec/request_spec.rb +55 -0
  46. data/spec/response_spec.rb +131 -0
  47. data/spec/session_spec.rb +35 -0
  48. data/spec/spec_helper.rb +89 -0
  49. data/spec/version_spec.rb +8 -0
  50. metadata +148 -0
@@ -0,0 +1,124 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+
4
+ NAME = 'roda'
5
+ VERS = lambda do
6
+ require File.expand_path("../lib/roda.rb", __FILE__)
7
+ Roda::RodaVersion
8
+ end
9
+ CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc"]
10
+
11
+ # Gem Packaging and Release
12
+
13
+ desc "Packages #{NAME}"
14
+ task :package=>[:clean] do |p|
15
+ sh %{gem build #{NAME}.gemspec}
16
+ end
17
+
18
+ ### RDoc
19
+
20
+ RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Roda: Routing tree web framework']
21
+
22
+ begin
23
+ gem 'hanna-nouveau'
24
+ RDOC_DEFAULT_OPTS.concat(['-f', 'hanna'])
25
+ rescue Gem::LoadError
26
+ end
27
+
28
+ rdoc_task_class = begin
29
+ require "rdoc/task"
30
+ RDoc::Task
31
+ rescue LoadError
32
+ require "rake/rdoctask"
33
+ Rake::RDocTask
34
+ end
35
+
36
+ RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
37
+ RDOC_FILES = %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb"
38
+
39
+ rdoc_task_class.new do |rdoc|
40
+ rdoc.rdoc_dir = "rdoc"
41
+ rdoc.options += RDOC_OPTS
42
+ rdoc.rdoc_files.add RDOC_FILES
43
+ end
44
+
45
+ rdoc_task_class.new(:website_rdoc) do |rdoc|
46
+ rdoc.rdoc_dir = "www/public/rdoc"
47
+ rdoc.options += RDOC_OPTS
48
+ rdoc.rdoc_files.add RDOC_FILES
49
+ end
50
+
51
+ ### Website
52
+
53
+ desc "Make local version of website"
54
+ task :website_base do
55
+ sh %{#{FileUtils::RUBY} www/make_www.rb}
56
+ end
57
+
58
+ desc "Make local version of website, with rdoc"
59
+ task :website => [:website_base, :website_rdoc]
60
+
61
+ desc "Make local version of website"
62
+ task :serve => :website do
63
+ sh %{#{FileUtils::RUBY} -C www -S rackup}
64
+ end
65
+
66
+
67
+ ### Specs
68
+
69
+ begin
70
+ begin
71
+ # RSpec 1
72
+ require "spec/rake/spectask"
73
+ spec_class = Spec::Rake::SpecTask
74
+ spec_files_meth = :spec_files=
75
+ rescue LoadError
76
+ # RSpec 2
77
+ require "rspec/core/rake_task"
78
+ spec_class = RSpec::Core::RakeTask
79
+ spec_files_meth = :pattern=
80
+ end
81
+
82
+ spec = lambda do |name, files, d|
83
+ lib_dir = File.join(File.dirname(File.expand_path(__FILE__)), 'lib')
84
+ ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)
85
+ desc d
86
+ spec_class.new(name) do |t|
87
+ t.send spec_files_meth, files
88
+ t.spec_opts = ENV["#{NAME.upcase}_SPEC_OPTS"].split if ENV["#{NAME.upcase}_SPEC_OPTS"]
89
+ end
90
+ end
91
+
92
+ spec_with_cov = lambda do |name, files, d|
93
+ spec.call(name, files, d)
94
+ desc "#{d} with coverage"
95
+ task "#{name}_cov" do
96
+ ENV['COVERAGE'] = '1'
97
+ Rake::Task[name].invoke
98
+ end
99
+ end
100
+
101
+ task :default => [:spec]
102
+ spec_with_cov.call("spec", Dir["spec/*_spec.rb"] + Dir["spec/plugin/*_spec.rb"], "Run specs")
103
+ rescue LoadError
104
+ task :default do
105
+ puts "Must install rspec to run the default task (which runs specs)"
106
+ end
107
+ end
108
+
109
+ ### Other
110
+
111
+ desc "Print #{NAME} version"
112
+ task :version do
113
+ puts VERS.call
114
+ end
115
+
116
+ desc "Start an IRB shell using the extension"
117
+ task :irb do
118
+ require 'rbconfig'
119
+ ruby = ENV['RUBY'] || File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
120
+ irb = ENV['IRB'] || File.join(RbConfig::CONFIG['bindir'], File.basename(ruby).sub('ruby', 'irb'))
121
+ sh %{#{irb} -I lib -r #{NAME}}
122
+ end
123
+
124
+
@@ -0,0 +1,608 @@
1
+ require "rack"
2
+ require "thread"
3
+
4
+ # The main class for Roda. Roda is built completely out of plugins, with the
5
+ # default plugin being Roda::RodaPlugins::Base, so this class is mostly empty
6
+ # except for some constants.
7
+ class Roda
8
+ # Roda's version, always specified by a string in \d+\.\d+\.\d+ format.
9
+ RodaVersion = '0.9.0'.freeze
10
+
11
+ # Error class raised by Roda
12
+ class RodaError < StandardError; end
13
+
14
+ # Base class used for Roda requests. The instance methods for this
15
+ # class are added by Roda::RodaPlugins::Base::RequestMethods, so this
16
+ # only contains the class methods.
17
+ class RodaRequest < ::Rack::Request;
18
+ @roda_class = ::Roda
19
+
20
+ class << self
21
+ # Reference to the Roda class related to this request class.
22
+ attr_accessor :roda_class
23
+
24
+ # Since RodaRequest is anonymously subclassed when Roda is subclassed,
25
+ # and then assigned to a constant of the Roda subclass, make inspect
26
+ # reflect the likely name for the class.
27
+ def inspect
28
+ "#{roda_class.inspect}::RodaRequest"
29
+ end
30
+ end
31
+ end
32
+
33
+ # Base class used for Roda responses. The instance methods for this
34
+ # class are added by Roda::RodaPlugins::Base::ResponseMethods, so this
35
+ # only contains the class methods.
36
+ class RodaResponse < ::Rack::Response;
37
+ @roda_class = ::Roda
38
+
39
+ class << self
40
+ # Reference to the Roda class related to this response class.
41
+ attr_accessor :roda_class
42
+
43
+ # Since RodaResponse is anonymously subclassed when Roda is subclassed,
44
+ # and then assigned to a constant of the Roda subclass, make inspect
45
+ # reflect the likely name for the class.
46
+ def inspect
47
+ "#{roda_class.inspect}::RodaResponse"
48
+ end
49
+ end
50
+ end
51
+
52
+ @builder = ::Rack::Builder.new
53
+ @middleware = []
54
+ @opts = {}
55
+
56
+ # Module in which all Roda plugins should be stored. Also contains logic for
57
+ # registering and loading plugins.
58
+ module RodaPlugins
59
+ # Mutex protecting the plugins hash
60
+ @mutex = ::Mutex.new
61
+
62
+ # Stores registered plugins
63
+ @plugins = {}
64
+
65
+ # If the registered plugin already exists, use it. Otherwise,
66
+ # require it and return it. This raises a LoadError if such a
67
+ # plugin doesn't exist, or a RodaError if it exists but it does
68
+ # not register itself correctly.
69
+ def self.load_plugin(name)
70
+ h = @plugins
71
+ unless plugin = @mutex.synchronize{h[name]}
72
+ require "roda/plugins/#{name}"
73
+ raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = @mutex.synchronize{h[name]}
74
+ end
75
+ plugin
76
+ end
77
+
78
+ # Register the given plugin with Roda, so that it can be loaded using #plugin
79
+ # with a symbol. Should be used by plugin files.
80
+ def self.register_plugin(name, mod)
81
+ @mutex.synchronize{@plugins[name] = mod}
82
+ end
83
+
84
+ # The base plugin for Roda, implementing all default functionality.
85
+ # Methods are put into a plugin so future plugins can easily override
86
+ # them and call super to get the default behavior.
87
+ module Base
88
+ # Class methods for the Roda class.
89
+ module ClassMethods
90
+ # The rack application that this class uses.
91
+ attr_reader :app
92
+
93
+ # The settings/options hash for the current class.
94
+ attr_reader :opts
95
+
96
+ # Call the internal rack application with the given environment.
97
+ # This allows the class itself to be used as a rack application.
98
+ # However, for performance, it's better to use #app to get direct
99
+ # access to the underlying rack app.
100
+ def call(env)
101
+ app.call(env)
102
+ end
103
+
104
+ # When inheriting Roda, setup a new rack app builder, copy the
105
+ # default middleware and opts into the subclass, and set the
106
+ # request and response classes in the subclasses to be subclasses
107
+ # of the request and responses classes in the parent class. This
108
+ # makes it so child classes inherit plugins from their parent,
109
+ # but using plugins in child classes does not affect the parent.
110
+ def inherited(subclass)
111
+ super
112
+ subclass.instance_variable_set(:@builder, ::Rack::Builder.new)
113
+ subclass.instance_variable_set(:@middleware, @middleware.dup)
114
+ subclass.instance_variable_set(:@opts, opts.dup)
115
+
116
+ request_class = Class.new(self::RodaRequest)
117
+ request_class.roda_class = subclass
118
+ subclass.const_set(:RodaRequest, request_class)
119
+
120
+ response_class = Class.new(self::RodaResponse)
121
+ response_class.roda_class = subclass
122
+ subclass.const_set(:RodaResponse, response_class)
123
+ end
124
+
125
+ # Load a new plugin into the current class. A plugin can be a module
126
+ # which is used directly, or a symbol represented a registered plugin
127
+ # which will be required and then used.
128
+ def plugin(mixin, *args, &block)
129
+ if mixin.is_a?(Symbol)
130
+ mixin = RodaPlugins.load_plugin(mixin)
131
+ end
132
+
133
+ if mixin.respond_to?(:load_dependencies)
134
+ mixin.load_dependencies(self, *args, &block)
135
+ end
136
+
137
+ if defined?(mixin::InstanceMethods)
138
+ include mixin::InstanceMethods
139
+ end
140
+ if defined?(mixin::ClassMethods)
141
+ extend mixin::ClassMethods
142
+ end
143
+ if defined?(mixin::RequestMethods)
144
+ self::RodaRequest.send(:include, mixin::RequestMethods)
145
+ end
146
+ if defined?(mixin::ResponseMethods)
147
+ self::RodaResponse.send(:include, mixin::ResponseMethods)
148
+ end
149
+
150
+ if mixin.respond_to?(:configure)
151
+ mixin.configure(self, *args, &block)
152
+ end
153
+ end
154
+
155
+ # Include the given module in the request class. If a block
156
+ # is provided instead of a module, create a module using the
157
+ # the block.
158
+ def request_module(mod = nil, &block)
159
+ module_include(:request, mod, &block)
160
+ end
161
+
162
+ # Include the given module in the response class. If a block
163
+ # is provided instead of a module, create a module using the
164
+ # the block.
165
+ def response_module(mod = nil, &block)
166
+ module_include(:response, mod, &block)
167
+ end
168
+
169
+ # Setup route definitions for the current class, and build the
170
+ # rack application using the stored middleware.
171
+ def route(&block)
172
+ @middleware.each{|a, b| @builder.use(*a, &b)}
173
+ @builder.run lambda{|env| new.call(env, &block)}
174
+ @app = @builder.to_app
175
+ end
176
+
177
+ # Add a middleware to use for the rack application. Must be
178
+ # called before calling #route.
179
+ def use(*args, &block)
180
+ @middleware << [args, block]
181
+ end
182
+
183
+ private
184
+
185
+ # Backbone of the request_module and response_module support.
186
+ def module_include(type, mod)
187
+ if type == :response
188
+ klass = self::RodaResponse
189
+ iv = :@response_module
190
+ else
191
+ klass = self::RodaRequest
192
+ iv = :@request_module
193
+ end
194
+
195
+ if mod
196
+ raise RodaError, "can't provide both argument and block to response_module" if block_given?
197
+ klass.send(:include, mod)
198
+ else
199
+ unless mod = instance_variable_get(iv)
200
+ mod = instance_variable_set(iv, Module.new)
201
+ klass.send(:include, mod)
202
+ end
203
+
204
+ mod.module_eval(&Proc.new) if block_given?
205
+ end
206
+
207
+ mod
208
+ end
209
+ end
210
+
211
+ # Instance methods for the Roda class.
212
+ module InstanceMethods
213
+ SESSION_KEY = 'rack.session'.freeze
214
+
215
+ # Create a request and response of the appopriate
216
+ # class, the instance_exec the route block with
217
+ # the request, handling any halts.
218
+ def call(env, &block)
219
+ @_request = self.class::RodaRequest.new(self, env)
220
+ @_response = self.class::RodaResponse.new
221
+ _route(&block)
222
+ end
223
+
224
+ # The environment for the current request.
225
+ def env
226
+ request.env
227
+ end
228
+
229
+ # The class-level options hash. This should probably not be
230
+ # modified at the instance level.
231
+ def opts
232
+ self.class.opts
233
+ end
234
+
235
+ # The instance of the request class related to this request.
236
+ def request
237
+ @_request
238
+ end
239
+
240
+ # The instance of the response class related to this request.
241
+ def response
242
+ @_response
243
+ end
244
+
245
+ # The session for the current request. Raises a RodaError if
246
+ # a session handler has not been loaded.
247
+ def session
248
+ env[SESSION_KEY] || raise(RodaError, "You're missing a session handler. You can get started by adding use Rack::Session::Cookie")
249
+ end
250
+
251
+ private
252
+
253
+ # Internals of #call, extracted so that plugins can override
254
+ # behavior after the request and response have been setup.
255
+ def _route(&block)
256
+ catch(:halt) do
257
+ request.handle_on_result(instance_exec(@_request, &block))
258
+ response.finish
259
+ end
260
+ end
261
+ end
262
+
263
+ # Instance methods for RodaRequest, mostly related to handling routing
264
+ # for the request.
265
+ module RequestMethods
266
+ PATH_INFO = "PATH_INFO".freeze
267
+ SCRIPT_NAME = "SCRIPT_NAME".freeze
268
+ REQUEST_METHOD = "REQUEST_METHOD".freeze
269
+ EMPTY_STRING = "".freeze
270
+ TERM = {:term=>true}.freeze
271
+ SEGMENT = "([^\\/]+)".freeze
272
+
273
+ # The current captures for the request. This gets modified as routing
274
+ # occurs.
275
+ attr_reader :captures
276
+
277
+ # The Roda instance related to this request object. Useful if routing
278
+ # methods need access to the scope of the Roda route block.
279
+ attr_reader :scope
280
+
281
+ # Store the roda instance and environment.
282
+ def initialize(scope, env)
283
+ @scope = scope
284
+ @captures = []
285
+ super(env)
286
+ end
287
+
288
+ # As request routing modifies SCRIPT_NAME and PATH_INFO, this exists
289
+ # as a helper method to get the full request of the path info.
290
+ def full_path_info
291
+ "#{env[SCRIPT_NAME]}#{env[PATH_INFO]}"
292
+ end
293
+
294
+ # If this is not a GET method, returns immediately. Otherwise, calls
295
+ # #is if there are any arguments, or #on if there are no arguments.
296
+ def get(*args, &block)
297
+ is_or_on(*args, &block) if get?
298
+ end
299
+
300
+ # Immediately stop execution of the route block and return the given
301
+ # rack response array of status, headers, and body.
302
+ def halt(response)
303
+ _halt(response)
304
+ end
305
+
306
+ # Handle #on block return values. By default, if a string is given
307
+ # and the response is empty, use the string as the response body.
308
+ def handle_on_result(result)
309
+ res = response
310
+ if result.is_a?(String) && res.empty?
311
+ res.write(result)
312
+ end
313
+ end
314
+
315
+ # Show information about current request, including request class,
316
+ # request method and full path.
317
+ def inspect
318
+ "#<#{self.class.inspect} #{env[REQUEST_METHOD]} #{full_path_info}>"
319
+ end
320
+
321
+ # Adds TERM as the final argument and passes to #on, ensuring that
322
+ # there is only a match if #on has fully matched the path.
323
+ def is(*args, &block)
324
+ args << TERM
325
+ on(*args, &block)
326
+ end
327
+
328
+ # Attempts to match on all of the arguments. If all of the
329
+ # arguments match, control is yielded to the block, and after
330
+ # the block returns, the rack response will be returned.
331
+ # If any of the arguments fails, ensures the request state is
332
+ # returned to that before matches were attempted.
333
+ def on(*args, &block)
334
+ try do
335
+ # We stop evaluation of this entire matcher unless
336
+ # each and every `arg` defined for this matcher evaluates
337
+ # to a non-false value.
338
+ #
339
+ # Short circuit examples:
340
+ # on true, false do
341
+ #
342
+ # # PATH_INFO=/user
343
+ # on true, "signup"
344
+ return unless args.all?{|arg| match(arg)}
345
+
346
+ # The captures we yield here were generated and assembled
347
+ # by evaluating each of the `arg`s above. Most of these
348
+ # are carried out by #consume.
349
+ handle_on_result(yield(*captures))
350
+
351
+ _halt response.finish
352
+ end
353
+ end
354
+
355
+ # If this is not a POST method, returns immediately. Otherwise, calls
356
+ # #is if there are any arguments, or #on if there are no arguments.
357
+ def post(*args, &block)
358
+ is_or_on(*args, &block) if post?
359
+ end
360
+
361
+ # The response related to the current request.
362
+ def response
363
+ scope.response
364
+ end
365
+
366
+ # Immediately redirect to the given path.
367
+ def redirect(path, status=302)
368
+ response.redirect(path, status)
369
+ _halt response.finish
370
+ end
371
+
372
+ # Call the given rack app with the environment and immediately return
373
+ # the response as the response for this request.
374
+ def run(app)
375
+ _halt app.call(env)
376
+ end
377
+
378
+ private
379
+
380
+ # Internal halt method, used so that halt can be overridden to handle
381
+ # non-rack response arrays, but internal code that always generates
382
+ # rack response arrays can use this for performance.
383
+ def _halt(response)
384
+ throw :halt, response
385
+ end
386
+
387
+ # Attempts to match the pattern to the current path. If there is no
388
+ # match, returns false without changes. Otherwise, modifies
389
+ # SCRIPT_NAME to include the matched path, removes the matched
390
+ # path from PATH_INFO, and updates captures with any regex captures.
391
+ def consume(pattern)
392
+ matchdata = env[PATH_INFO].match(/\A(\/(?:#{pattern}))(\/|\z)/)
393
+
394
+ return false unless matchdata
395
+
396
+ vars = matchdata.captures
397
+
398
+ # Don't mutate SCRIPT_NAME, breaks try
399
+ env[SCRIPT_NAME] += vars.shift
400
+ env[PATH_INFO] = "#{vars.pop}#{matchdata.post_match}"
401
+
402
+ captures.concat(vars)
403
+ end
404
+
405
+ # Backbone of the verb method support, calling #is if there are any
406
+ # arguments, or #on if there are none.
407
+ def is_or_on(*args, &block)
408
+ if args.empty?
409
+ on(*args, &block)
410
+ else
411
+ is(*args, &block)
412
+ end
413
+ end
414
+
415
+ # Attempt to match the argument to the given request, handling
416
+ # common ruby types.
417
+ def match(matcher)
418
+ case matcher
419
+ when String
420
+ match_string(matcher)
421
+ when Regexp
422
+ consume(matcher)
423
+ when Symbol
424
+ consume(SEGMENT)
425
+ when Hash
426
+ matcher.all?{|k,v| send("match_#{k}", v)}
427
+ when Array
428
+ match_array(matcher)
429
+ when Proc
430
+ matcher.call
431
+ else
432
+ matcher
433
+ end
434
+ end
435
+
436
+ # Match any of the elements in the given array. Return at the
437
+ # first match without evaluating future matches. Returns false
438
+ # if no elements in the array match.
439
+ def match_array(matcher)
440
+ matcher.any? do |m|
441
+ if matched = match(m)
442
+ if m.is_a?(String)
443
+ captures.push(m)
444
+ end
445
+ end
446
+
447
+ matched
448
+ end
449
+ end
450
+
451
+ # Match files with the given extension. Requires that the
452
+ # request path end with the extension.
453
+ def match_extension(ext)
454
+ consume("([^\\/]+?)\.#{ext}\\z")
455
+ end
456
+
457
+ # Match by request method. This can be an array if you want
458
+ # to match on multiple methods.
459
+ def match_method(type)
460
+ if type.is_a?(Array)
461
+ type.any?{|t| match_method(t)}
462
+ else
463
+ type.to_s.upcase == env[REQUEST_METHOD]
464
+ end
465
+ end
466
+
467
+ # Match the given parameter if present, even if the parameter is empty.
468
+ # Adds any match to the captures.
469
+ def match_param(key)
470
+ if v = self[key]
471
+ captures << v
472
+ end
473
+ end
474
+
475
+ # Match the given parameter if present and not empty.
476
+ # Adds any match to the captures.
477
+ def match_param!(key)
478
+ if (v = self[key]) && !v.empty?
479
+ captures << v
480
+ end
481
+ end
482
+
483
+ # Match the given string to the request path. Regexp escapes the
484
+ # string so that regexp metacharacters are not matched, and recognizes
485
+ # colon tokens for placeholders.
486
+ def match_string(str)
487
+ str = Regexp.escape(str)
488
+ str.gsub!(/:\w+/, SEGMENT)
489
+ consume(str)
490
+ end
491
+
492
+ # Only match if the request path is empty, which usually indicates it
493
+ # has already been fully matched.
494
+ def match_term(term)
495
+ !(term ^ (env[PATH_INFO] == EMPTY_STRING))
496
+ end
497
+
498
+ # Yield to the given block, clearing any captures before
499
+ # yielding and restoring the SCRIPT_NAME and PATH_INFO on exit.
500
+ def try
501
+ script = env[SCRIPT_NAME]
502
+ path = env[PATH_INFO]
503
+
504
+ # For every block, we make sure to reset captures so that
505
+ # nesting matchers won't mess with each other's captures.
506
+ captures.clear
507
+
508
+ yield
509
+
510
+ ensure
511
+ env[SCRIPT_NAME] = script
512
+ env[PATH_INFO] = path
513
+ end
514
+ end
515
+
516
+ # Instance methods for RodaResponse
517
+ module ResponseMethods
518
+ CONTENT_LENGTH = "Content-Length".freeze
519
+ CONTENT_TYPE = "Content-Type".freeze
520
+ DEFAULT_CONTENT_TYPE = "text/html".freeze
521
+ LOCATION = "Location".freeze
522
+
523
+ # The status code to use for the response. If none is given, will use 200
524
+ # code for non-empty responses and a 404 code for empty responses.
525
+ attr_accessor :status
526
+
527
+ # The hash of response headers for the current response.
528
+ attr_reader :headers
529
+
530
+ # Set the default headers when creating a response.
531
+ def initialize
532
+ @status = nil
533
+ @headers = default_headers
534
+ @body = []
535
+ @length = 0
536
+ end
537
+
538
+ # Return the response header with the given key.
539
+ def [](key)
540
+ @headers[key]
541
+ end
542
+
543
+ # Set the response header with the given key to the given value.
544
+ def []=(key, value)
545
+ @headers[key] = value
546
+ end
547
+
548
+ # Show response class, status code, response headers, and response body
549
+ def inspect
550
+ "#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>"
551
+ end
552
+
553
+ # The default headers to use for responses.
554
+ def default_headers
555
+ {CONTENT_TYPE => DEFAULT_CONTENT_TYPE}
556
+ end
557
+
558
+ # Modify the headers to include a Set-Cookie value that
559
+ # deletes the cookie. A value hash can be provided to
560
+ # override the default one used to delete the cookie.
561
+ def delete_cookie(key, value = {})
562
+ ::Rack::Utils.delete_cookie_header!(@headers, key, value)
563
+ end
564
+
565
+ # Whether the response body has been written to yet. Note
566
+ # that writing an empty string to the response body marks
567
+ # the response as not empty.
568
+ def empty?
569
+ @body.empty?
570
+ end
571
+
572
+ # Return the rack response array of status, headers, and body
573
+ # for the current response.
574
+ def finish
575
+ b = @body
576
+ s = (@status ||= b.empty? ? 404 : 200)
577
+ [s, @headers, b]
578
+ end
579
+
580
+ # Set the Location header to the given path, and the status
581
+ # to the given status.
582
+ def redirect(path, status = 302)
583
+ @headers[LOCATION] = path
584
+ @status = status
585
+ end
586
+
587
+ # Set the cookie with the given key in the headers.
588
+ def set_cookie(key, value)
589
+ ::Rack::Utils.set_cookie_header!(@headers, key, value)
590
+ end
591
+
592
+ # Write to the response body. Updates Content-Length header
593
+ # with the size of the string written. Returns nil.
594
+ def write(str)
595
+ s = str.to_s
596
+
597
+ @length += s.bytesize
598
+ @headers[CONTENT_LENGTH] = @length.to_s
599
+ @body << s
600
+ nil
601
+ end
602
+ end
603
+ end
604
+ end
605
+
606
+ extend RodaPlugins::Base::ClassMethods
607
+ plugin RodaPlugins::Base
608
+ end