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.
- checksums.yaml +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +709 -0
- data/Rakefile +124 -0
- data/lib/roda.rb +608 -0
- data/lib/roda/plugins/all_verbs.rb +48 -0
- data/lib/roda/plugins/default_headers.rb +50 -0
- data/lib/roda/plugins/error_handler.rb +69 -0
- data/lib/roda/plugins/flash.rb +62 -0
- data/lib/roda/plugins/h.rb +24 -0
- data/lib/roda/plugins/halt.rb +79 -0
- data/lib/roda/plugins/header_matchers.rb +57 -0
- data/lib/roda/plugins/hooks.rb +106 -0
- data/lib/roda/plugins/indifferent_params.rb +47 -0
- data/lib/roda/plugins/middleware.rb +88 -0
- data/lib/roda/plugins/multi_route.rb +77 -0
- data/lib/roda/plugins/not_found.rb +62 -0
- data/lib/roda/plugins/pass.rb +34 -0
- data/lib/roda/plugins/render.rb +217 -0
- data/lib/roda/plugins/streaming.rb +165 -0
- data/spec/composition_spec.rb +19 -0
- data/spec/env_spec.rb +11 -0
- data/spec/integration_spec.rb +63 -0
- data/spec/matchers_spec.rb +658 -0
- data/spec/module_spec.rb +29 -0
- data/spec/opts_spec.rb +42 -0
- data/spec/plugin/all_verbs_spec.rb +29 -0
- data/spec/plugin/default_headers_spec.rb +63 -0
- data/spec/plugin/error_handler_spec.rb +67 -0
- data/spec/plugin/flash_spec.rb +59 -0
- data/spec/plugin/h_spec.rb +13 -0
- data/spec/plugin/halt_spec.rb +62 -0
- data/spec/plugin/header_matchers_spec.rb +61 -0
- data/spec/plugin/hooks_spec.rb +97 -0
- data/spec/plugin/indifferent_params_spec.rb +13 -0
- data/spec/plugin/middleware_spec.rb +52 -0
- data/spec/plugin/multi_route_spec.rb +98 -0
- data/spec/plugin/not_found_spec.rb +99 -0
- data/spec/plugin/pass_spec.rb +23 -0
- data/spec/plugin/render_spec.rb +148 -0
- data/spec/plugin/streaming_spec.rb +52 -0
- data/spec/plugin_spec.rb +61 -0
- data/spec/redirect_spec.rb +24 -0
- data/spec/request_spec.rb +55 -0
- data/spec/response_spec.rb +131 -0
- data/spec/session_spec.rb +35 -0
- data/spec/spec_helper.rb +89 -0
- data/spec/version_spec.rb +8 -0
- metadata +148 -0
data/Rakefile
ADDED
@@ -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
|
+
|
data/lib/roda.rb
ADDED
@@ -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
|