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