roda-cj 0.9.1

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