roda 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +709 -0
  5. data/Rakefile +124 -0
  6. data/lib/roda.rb +608 -0
  7. data/lib/roda/plugins/all_verbs.rb +48 -0
  8. data/lib/roda/plugins/default_headers.rb +50 -0
  9. data/lib/roda/plugins/error_handler.rb +69 -0
  10. data/lib/roda/plugins/flash.rb +62 -0
  11. data/lib/roda/plugins/h.rb +24 -0
  12. data/lib/roda/plugins/halt.rb +79 -0
  13. data/lib/roda/plugins/header_matchers.rb +57 -0
  14. data/lib/roda/plugins/hooks.rb +106 -0
  15. data/lib/roda/plugins/indifferent_params.rb +47 -0
  16. data/lib/roda/plugins/middleware.rb +88 -0
  17. data/lib/roda/plugins/multi_route.rb +77 -0
  18. data/lib/roda/plugins/not_found.rb +62 -0
  19. data/lib/roda/plugins/pass.rb +34 -0
  20. data/lib/roda/plugins/render.rb +217 -0
  21. data/lib/roda/plugins/streaming.rb +165 -0
  22. data/spec/composition_spec.rb +19 -0
  23. data/spec/env_spec.rb +11 -0
  24. data/spec/integration_spec.rb +63 -0
  25. data/spec/matchers_spec.rb +658 -0
  26. data/spec/module_spec.rb +29 -0
  27. data/spec/opts_spec.rb +42 -0
  28. data/spec/plugin/all_verbs_spec.rb +29 -0
  29. data/spec/plugin/default_headers_spec.rb +63 -0
  30. data/spec/plugin/error_handler_spec.rb +67 -0
  31. data/spec/plugin/flash_spec.rb +59 -0
  32. data/spec/plugin/h_spec.rb +13 -0
  33. data/spec/plugin/halt_spec.rb +62 -0
  34. data/spec/plugin/header_matchers_spec.rb +61 -0
  35. data/spec/plugin/hooks_spec.rb +97 -0
  36. data/spec/plugin/indifferent_params_spec.rb +13 -0
  37. data/spec/plugin/middleware_spec.rb +52 -0
  38. data/spec/plugin/multi_route_spec.rb +98 -0
  39. data/spec/plugin/not_found_spec.rb +99 -0
  40. data/spec/plugin/pass_spec.rb +23 -0
  41. data/spec/plugin/render_spec.rb +148 -0
  42. data/spec/plugin/streaming_spec.rb +52 -0
  43. data/spec/plugin_spec.rb +61 -0
  44. data/spec/redirect_spec.rb +24 -0
  45. data/spec/request_spec.rb +55 -0
  46. data/spec/response_spec.rb +131 -0
  47. data/spec/session_spec.rb +35 -0
  48. data/spec/spec_helper.rb +89 -0
  49. data/spec/version_spec.rb +8 -0
  50. metadata +148 -0
@@ -0,0 +1,48 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The all_verbs plugin adds methods for http verbs other than
4
+ # get and post. The following verbs are added, assuming
5
+ # rack handles them: delete, head, options, link, patch, put,
6
+ # trace, unlink.
7
+ #
8
+ # These methods operate just like Roda's default get and post
9
+ # methods other that the http verb used, so using them without
10
+ # any arguments just checks for the request method, while
11
+ # using them with any arguments also checks that the arguments
12
+ # match the full path.
13
+ #
14
+ # Example:
15
+ #
16
+ # plugin :all_verbs
17
+ #
18
+ # route do |r|
19
+ # r.delete
20
+ # # Handle DELETE
21
+ # end
22
+ # r.put do
23
+ # # Handle PUT
24
+ # end
25
+ # r.patch do
26
+ # # Handle PATCH
27
+ # end
28
+ # end
29
+ #
30
+ # The verb methods are defined via metaprogramming, so there
31
+ # isn't documentation for the individual methods created.
32
+ module AllVerbs
33
+ module RequestMethods
34
+ %w'delete head options link patch put trace unlink'.each do |t|
35
+ if ::Rack::Request.method_defined?("#{t}?")
36
+ class_eval(<<-END, __FILE__, __LINE__+1)
37
+ def #{t}(*args, &block)
38
+ is_or_on(*args, &block) if #{t}?
39
+ end
40
+ END
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ register_plugin(:all_verbs, AllVerbs)
47
+ end
48
+ end
@@ -0,0 +1,50 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The default_headers plugin accepts a hash of headers,
4
+ # and overrides the default_headers method in the
5
+ # response class to be a copy of the headers.
6
+ #
7
+ # Note that when using this module, you should not
8
+ # attempt to mutate of the values set in the default
9
+ # headers hash.
10
+ #
11
+ # Example:
12
+ #
13
+ # plugin :default_headers, 'Content-Type'=>'text/csv'
14
+ #
15
+ # You can also modify the default headers later:
16
+ #
17
+ # plugin :default_headers
18
+ # default_headers['Foo'] = 'bar'
19
+ # default_headers.merge!('Bar'=>'baz')
20
+ module DefaultHeaders
21
+ # Merge the given headers into the existing default headers, if any.
22
+ def self.configure(app, headers={})
23
+ app.instance_eval do
24
+ @default_headers ||= {}
25
+ @default_headers.merge!(headers)
26
+ end
27
+ end
28
+
29
+ module ClassMethods
30
+ # The default response headers to use for the current class.
31
+ attr_reader :default_headers
32
+
33
+ # Copy the default headers into the subclass when inheriting.
34
+ def inherited(subclass)
35
+ super
36
+ subclass.instance_variable_set(:@default_headers, default_headers.dup)
37
+ end
38
+ end
39
+
40
+ module ResponseMethods
41
+ # Get the default headers from the related roda class.
42
+ def default_headers
43
+ self.class.roda_class.default_headers.dup
44
+ end
45
+ end
46
+ end
47
+
48
+ register_plugin(:default_headers, DefaultHeaders)
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The error_handler plugin adds an error handler to the routing,
4
+ # so that if routing the request raises an error, a nice error
5
+ # message page can be returned to the user.
6
+ #
7
+ # You can provide the error handler as a block to the plugin:
8
+ #
9
+ # plugin :error_handler do |e|
10
+ # "Oh No!"
11
+ # end
12
+ #
13
+ # Or later via the +error+ class method:
14
+ #
15
+ # plugin :error_handler
16
+ #
17
+ # error do |e|
18
+ # "Oh No!"
19
+ # end
20
+ #
21
+ # In both cases, the exception instance is passed into the block,
22
+ # and the block can return the request body via a string.
23
+ #
24
+ # If an exception is raised, the response status will be set to 500
25
+ # before executing the error handler. The error handler can change
26
+ # the response status if necessary.
27
+ module ErrorHandler
28
+ # If a block is given, automatically call the +error+ method on
29
+ # the Roda class with it.
30
+ def self.configure(app, &block)
31
+ if block
32
+ app.error(&block)
33
+ end
34
+ end
35
+
36
+ module ClassMethods
37
+ # Install the given block as the error handler, so that if routing
38
+ # the request raises an exception, the block will be called with
39
+ # the exception in the scope of the Roda instance.
40
+ def error(&block)
41
+ define_method(:handle_error, &block)
42
+ private :handle_error
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+ private
48
+
49
+ # If an error occurs, set the response status to 500 and call
50
+ # the error handler.
51
+ def _route
52
+ super
53
+ rescue => e
54
+ response.status = 500
55
+ super{handle_error(e)}
56
+ end
57
+
58
+ # By default, have the error handler reraise the error, so using
59
+ # the plugin without installing an error handler doesn't change
60
+ # behavior.
61
+ def handle_error(e)
62
+ raise e
63
+ end
64
+ end
65
+ end
66
+
67
+ register_plugin(:error_handler, ErrorHandler)
68
+ end
69
+ end
@@ -0,0 +1,62 @@
1
+ require 'sinatra/flash/hash'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The flash plugin adds a +flash+ instance method to Roda,
6
+ # for typical web application flash handling, where values
7
+ # set in the current flash hash are available in the next
8
+ # request.
9
+ #
10
+ # With the example below, if a POST request is submitted,
11
+ # it will redirect and the resulting GET request will
12
+ # return 'b'.
13
+ #
14
+ # plugin :flash
15
+ #
16
+ # route do |r|
17
+ # r.is '' do
18
+ # r.get do
19
+ # flash['a']
20
+ # end
21
+ #
22
+ # r.post do
23
+ # flash['a'] = 'b'
24
+ # r.redirect('')
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ # The flash plugin uses sinatra-flash internally, so you
30
+ # must install sinatra-flash in order to use it.
31
+ module Flash
32
+ FlashHash = ::Sinatra::Flash::FlashHash
33
+
34
+ module InstanceMethods
35
+ # The internal session key used to store the flash.
36
+ KEY = :flash
37
+
38
+ # Access the flash hash for the current request, loading
39
+ # it from the session if it is not already loaded.
40
+ def flash
41
+ @_flash ||= FlashHash.new((session ? session[KEY] : {}))
42
+ end
43
+
44
+ private
45
+
46
+ # If the routing doesn't raise an error, rotate the flash
47
+ # hash in the session so the next request has access to it.
48
+ def _route
49
+ res = super
50
+
51
+ if f = @_flash
52
+ session[KEY] = f.next
53
+ end
54
+
55
+ res
56
+ end
57
+ end
58
+ end
59
+
60
+ register_plugin(:flash, Flash)
61
+ end
62
+ end
@@ -0,0 +1,24 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The h plugin adds an +h+ instance method that will HTML
4
+ # escape the input and return it.
5
+ #
6
+ # The following example will return "&lt;foo&gt;" as the body.
7
+ #
8
+ # plugin :h
9
+ #
10
+ # route do |r|
11
+ # h('<foo>')
12
+ # end
13
+ module H
14
+ module InstanceMethods
15
+ # HTML escape the input and return the escaped version.
16
+ def h(s)
17
+ ::Rack::Utils.escape_html(s.to_s)
18
+ end
19
+ end
20
+ end
21
+
22
+ register_plugin(:h, H)
23
+ end
24
+ end
@@ -0,0 +1,79 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The halt plugin augments the standard request +halt+ method to handle more than
4
+ # just rack response arrays.
5
+ #
6
+ # After loading the halt plugin:
7
+ #
8
+ # plugin :halt
9
+ #
10
+ # You can call halt with no arguments to immediately stop processing:
11
+ #
12
+ # route do |r|
13
+ # r.halt
14
+ # end
15
+ #
16
+ # You can call the halt method with an integer to set the response status and return:
17
+ #
18
+ # route do |r|
19
+ # r.halt(403)
20
+ # end
21
+ #
22
+ # Or set the response body and return:
23
+ #
24
+ # route do |r|
25
+ # r.halt("body')
26
+ # end
27
+ #
28
+ # Or set both:
29
+ #
30
+ # route do |r|
31
+ # r.halt(403, "body')
32
+ # end
33
+ #
34
+ # Or set response status, headers, and body:
35
+ #
36
+ # route do |r|
37
+ # r.halt(403, {'Content-Type'=>'text/csv'}, "body')
38
+ # end
39
+ #
40
+ # Note that there is a difference between provide status, headers, and body as separate
41
+ # arguments and providing them as a rack response array. With a rack response array,
42
+ # the values are used directly, while with 3 arguments, the headers given are merged into
43
+ # the existing headers and the given body is written to the existing response body.
44
+ module Halt
45
+ module RequestMethods
46
+ # Expand default halt method to handle status codes, headers, and bodies. See Halt.
47
+ def halt(*res)
48
+ case res.length
49
+ when 0 # do nothing
50
+ when 1
51
+ case v = res[0]
52
+ when Integer
53
+ response.status = v
54
+ when String
55
+ response.write v
56
+ when Array
57
+ super
58
+ else
59
+ raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
60
+ end
61
+ when 2
62
+ response.status = res[0]
63
+ response.write res[1]
64
+ when 3
65
+ response.status = res[0]
66
+ response.headers.merge!(res[1])
67
+ response.write res[2]
68
+ else
69
+ raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
70
+ end
71
+
72
+ _halt response.finish
73
+ end
74
+ end
75
+ end
76
+
77
+ register_plugin(:halt, Halt)
78
+ end
79
+ end
@@ -0,0 +1,57 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The header_matchers plugin adds hash matchers for matching on less-common
4
+ # HTTP headers.
5
+ #
6
+ # plugin :header_matchers
7
+ #
8
+ # It adds a +:header+ matcher for matching on arbitrary headers, which matches
9
+ # if the header is present:
10
+ #
11
+ # route do |r|
12
+ # r.on :header=>'X-App-Token' do
13
+ # end
14
+ # end
15
+ #
16
+ # It adds a +:host+ matcher for matching by the host of the request:
17
+ #
18
+ # route do |r|
19
+ # r.on :host=>'foo.example.com' do
20
+ # end
21
+ # end
22
+ #
23
+ # It adds an +:accept+ matcher for matching based on the Accept header:
24
+ #
25
+ # route do |r|
26
+ # r.on :accept=>'text/csv' do
27
+ # end
28
+ # end
29
+ #
30
+ # Note that the accept matcher is very simple and cannot handle wildcards,
31
+ # priorities, or anything but a simple comma separated list of mime types.
32
+ module HeaderMatchers
33
+ module RequestMethods
34
+ private
35
+
36
+ # Match if the given mimetype is one of the accepted mimetypes.
37
+ def match_accept(mimetype)
38
+ if env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
39
+ response["Content-Type"] = mimetype
40
+ end
41
+ end
42
+
43
+ # Match if the given uppercase key is present inside the environment.
44
+ def match_header(key)
45
+ env[key.upcase.tr("-","_")]
46
+ end
47
+
48
+ # Match if the host of the request is the same as the hostname.
49
+ def match_host(hostname)
50
+ hostname === host
51
+ end
52
+ end
53
+ end
54
+
55
+ register_plugin(:header_matchers, HeaderMatchers)
56
+ end
57
+ end
@@ -0,0 +1,106 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The hooks plugin adds before and after hooks to the request cycle.
4
+ #
5
+ # plugin :hooks
6
+ #
7
+ # before do
8
+ # request.redirect('/login') unless logged_in?
9
+ # @time = Time.now
10
+ # end
11
+ #
12
+ # after do |res|
13
+ # logger.notice("Took #{Time.now - @time} seconds")
14
+ # end
15
+ #
16
+ # Note that in general, before hooks are not needed, since you can just
17
+ # run code at the top of the route block:
18
+ #
19
+ # route do |r|
20
+ # r.redirect('/login') unless logged_in?
21
+ # # ...
22
+ # end
23
+ #
24
+ # Note that the after hook is called with the rack response array
25
+ # of status, headers, and body. If it wants to change the response,
26
+ # it must mutate this argument, calling <tt>response.status=</tt> inside
27
+ # an after block will not affect the returned status.
28
+ #
29
+ # However, this code makes it easier to write after hooks, as well as
30
+ # handle cases where before hooks are added after the route block.
31
+ module Hooks
32
+ def self.configure(app)
33
+ @app.instance_exec do
34
+ @after ||= nil
35
+ @before ||= nil
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ # Add an after hook. If there is already an after hook defined,
41
+ # use a proc that instance_execs the existing after proc and
42
+ # then instance_execs the given after proc, so that the given
43
+ # after proc always executes after the previous one.
44
+ def after(&block)
45
+ if block
46
+ @after = if b = @after
47
+ @after = proc do |res|
48
+ instance_exec(res, &b)
49
+ instance_exec(res, &block)
50
+ end
51
+ else
52
+ block
53
+ end
54
+ end
55
+ @after
56
+ end
57
+
58
+ # Add a before hook. If there is already a before hook defined,
59
+ # use a proc that instance_execs the give before proc and
60
+ # then instance_execs the existing before proc, so that the given
61
+ # before proc always executes before the previous one.
62
+ def before(&block)
63
+ if block
64
+ @before = if b = @before
65
+ @before = proc do
66
+ instance_exec(&block)
67
+ instance_exec(&b)
68
+ end
69
+ else
70
+ block
71
+ end
72
+ end
73
+ @before
74
+ end
75
+
76
+ # Copy the before and after hooks into the subclasses
77
+ # when inheriting
78
+ def inherited(subclass)
79
+ super
80
+ subclass.instance_variable_set(:@before, @before)
81
+ subclass.instance_variable_set(:@after, @after)
82
+ end
83
+ end
84
+
85
+ module InstanceMethods
86
+ private
87
+
88
+ # Before routing, execute the before hooks, and
89
+ # execute the after hooks before returning.
90
+ def _route(*, &block)
91
+ if b = self.class.before
92
+ instance_exec(&b)
93
+ end
94
+
95
+ res = super
96
+ ensure
97
+ if b = self.class.after
98
+ instance_exec(res, &b)
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ register_plugin(:hooks, Hooks)
105
+ end
106
+ end