roda 0.9.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 +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