roda-cj 0.9.1

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 (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