roda 0.9.0 → 1.0.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +62 -0
  3. data/README.rdoc +362 -167
  4. data/Rakefile +2 -2
  5. data/doc/release_notes/1.0.0.txt +329 -0
  6. data/lib/roda.rb +553 -180
  7. data/lib/roda/plugins/_erubis_escaping.rb +28 -0
  8. data/lib/roda/plugins/all_verbs.rb +7 -9
  9. data/lib/roda/plugins/backtracking_array.rb +92 -0
  10. data/lib/roda/plugins/content_for.rb +46 -0
  11. data/lib/roda/plugins/csrf.rb +60 -0
  12. data/lib/roda/plugins/flash.rb +53 -7
  13. data/lib/roda/plugins/halt.rb +8 -14
  14. data/lib/roda/plugins/head.rb +56 -0
  15. data/lib/roda/plugins/header_matchers.rb +2 -2
  16. data/lib/roda/plugins/json.rb +84 -0
  17. data/lib/roda/plugins/multi_route.rb +50 -10
  18. data/lib/roda/plugins/not_allowed.rb +140 -0
  19. data/lib/roda/plugins/pass.rb +13 -6
  20. data/lib/roda/plugins/per_thread_caching.rb +70 -0
  21. data/lib/roda/plugins/render.rb +20 -33
  22. data/lib/roda/plugins/render_each.rb +61 -0
  23. data/lib/roda/plugins/symbol_matchers.rb +79 -0
  24. data/lib/roda/plugins/symbol_views.rb +40 -0
  25. data/lib/roda/plugins/view_subdirs.rb +53 -0
  26. data/lib/roda/version.rb +3 -0
  27. data/spec/matchers_spec.rb +61 -5
  28. data/spec/plugin/_erubis_escaping_spec.rb +29 -0
  29. data/spec/plugin/backtracking_array_spec.rb +38 -0
  30. data/spec/plugin/content_for_spec.rb +34 -0
  31. data/spec/plugin/csrf_spec.rb +49 -0
  32. data/spec/plugin/flash_spec.rb +69 -5
  33. data/spec/plugin/head_spec.rb +35 -0
  34. data/spec/plugin/json_spec.rb +50 -0
  35. data/spec/plugin/multi_route_spec.rb +22 -6
  36. data/spec/plugin/not_allowed_spec.rb +55 -0
  37. data/spec/plugin/pass_spec.rb +8 -2
  38. data/spec/plugin/per_thread_caching_spec.rb +28 -0
  39. data/spec/plugin/render_each_spec.rb +30 -0
  40. data/spec/plugin/render_spec.rb +7 -1
  41. data/spec/plugin/symbol_matchers_spec.rb +68 -0
  42. data/spec/plugin/symbol_views_spec.rb +32 -0
  43. data/spec/plugin/view_subdirs_spec.rb +45 -0
  44. data/spec/plugin_spec.rb +11 -1
  45. data/spec/redirect_spec.rb +21 -4
  46. data/spec/request_spec.rb +9 -0
  47. metadata +49 -5
@@ -0,0 +1,28 @@
1
+ require 'erubis'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The _erubis_escaping plugin is an internal plugin that provides a
6
+ # subclass of Erubis::EscapedEruby with a bugfix and an optimization.
7
+ module ErubisEscaping
8
+ # Optimized subclass that fixes escaping of postfix conditionals.
9
+ class Eruby < Erubis::EscapedEruby
10
+ # Set escaping class to a local variable, so you don't need a
11
+ # constant lookup per escape.
12
+ def convert_input(codebuf, input)
13
+ codebuf << '_erubis_xml_helper = Erubis::XmlHelper;'
14
+ super
15
+ end
16
+
17
+ # Fix bug in Erubis::EscapedEruby where postfix conditionals inside
18
+ # <%= %> are broken (e.g. <%= foo if bar %> ), and optimize by using
19
+ # a local variable instead of a constant lookup.
20
+ def add_expr_escaped(src, code)
21
+ src << " #{@bufvar} << _erubis_xml_helper.escape_xml((" << code << '));'
22
+ end
23
+ end
24
+ end
25
+
26
+ register_plugin(:_erubis_escaping, ErubisEscaping)
27
+ end
28
+ end
@@ -16,7 +16,7 @@ class Roda
16
16
  # plugin :all_verbs
17
17
  #
18
18
  # route do |r|
19
- # r.delete
19
+ # r.delete do
20
20
  # # Handle DELETE
21
21
  # end
22
22
  # r.put do
@@ -30,14 +30,12 @@ class Roda
30
30
  # The verb methods are defined via metaprogramming, so there
31
31
  # isn't documentation for the individual methods created.
32
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
33
+ def self.configure(app)
34
+ %w'delete head options link patch put trace unlink'.each do |v|
35
+ if ::Rack::Request.method_defined?("#{v}?")
36
+ app.request_module do
37
+ app::RodaRequest.def_verb_method(self, v)
38
+ end
41
39
  end
42
40
  end
43
41
  end
@@ -0,0 +1,92 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The backtracking_array plugin changes the handling of array
4
+ # matchers such that if one of the array entries matches, but
5
+ # a later match argument fails, it will backtrack and try the
6
+ # next entry in the array. For example, the following match
7
+ # block does not match +/a/b+ by default:
8
+ #
9
+ # r.is ['a', 'a/b'] do |path|
10
+ # # ...
11
+ # end
12
+ #
13
+ # This is because the <tt>'a'</tt> entry in the array matches, which
14
+ # makes the array match. However, the next matcher is the
15
+ # terminal matcher (since +r.is+ was used), and since the
16
+ # path is not terminal as it still contains +/b+ after
17
+ # matching <tt>'a'</tt>.
18
+ #
19
+ # With the backtracking_array plugin, when the terminal matcher
20
+ # fails, matching will go on to the next entry in the array,
21
+ # <tt>'a/b'</tt>, which will also match. Since <tt>'a/b'</tt>
22
+ # matches the path fully, the terminal matcher also matches,
23
+ # and the match block yields.
24
+ module BacktrackingArray
25
+ module RequestMethods
26
+ PATH_INFO = "PATH_INFO".freeze
27
+ SCRIPT_NAME = "SCRIPT_NAME".freeze
28
+
29
+ private
30
+
31
+ # When matching for a single array, after a successful
32
+ # array element match, attempt to match all remaining
33
+ # elements. If the remaining elements could not be
34
+ # matched, reset the state and continue to the next
35
+ # entry in the array.
36
+ def _match_array(arg, rest=nil)
37
+ return super unless rest
38
+ env = @env
39
+
40
+ script = env[SCRIPT_NAME]
41
+ path = env[PATH_INFO]
42
+ caps = captures.dup
43
+ arg.each do |v|
44
+ if match(v, rest)
45
+ if v.is_a?(String)
46
+ captures.push(v)
47
+ end
48
+
49
+ if match_all(rest)
50
+ return true
51
+ end
52
+
53
+ # Matching all remaining elements failed, reset state
54
+ captures.replace(caps)
55
+ env[SCRIPT_NAME] = script
56
+ env[PATH_INFO] = path
57
+ end
58
+ end
59
+ false
60
+ end
61
+
62
+ # If any of the args are an array, handle backtracking such
63
+ # that if a later matcher fails, we roll back to the current
64
+ # matcher and proceed to the next entry in the array.
65
+ def match_all(args)
66
+ args = args.dup
67
+ until args.empty?
68
+ arg = args.shift
69
+ if match(arg, args)
70
+ return true if arg.is_a?(Array)
71
+ else
72
+ return
73
+ end
74
+ end
75
+ true
76
+ end
77
+
78
+ # When matching an array, include the remaining arguments,
79
+ # otherwise, just match the single argument.
80
+ def match(v, rest = nil)
81
+ if v.is_a?(Array)
82
+ _match_array(v, rest)
83
+ else
84
+ super(v)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ register_plugin(:backtracking_array, BacktrackingArray)
91
+ end
92
+ end
@@ -0,0 +1,46 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The content_for plugin is designed to be used with the
4
+ # render plugin, allowing you to store content inside one
5
+ # template, and retrieve that content inside a separate
6
+ # template. Most commonly, this is so view templates
7
+ # can set content for the layout template to display outside
8
+ # of the normal content pane.
9
+ #
10
+ # The content_for template probably only works with erb
11
+ # templates, and requires that you don't override the
12
+ # +:outvar+ render option. In the template in which you
13
+ # want to store content, call content_for with a block:
14
+ #
15
+ # <% content_for :foo do %>
16
+ # Some content here.
17
+ # <% end %>
18
+ #
19
+ # In the template in which you want to retrieve content,
20
+ # call content_for without the block:
21
+ #
22
+ # <%= content_for :foo %>
23
+ module ContentFor
24
+ module InstanceMethods
25
+ # If called with a block, store content enclosed by block
26
+ # under the given key. If called without a block, retrieve
27
+ # stored content with the given key, or return nil if there
28
+ # is no content stored with that key.
29
+ def content_for(key, &block)
30
+ if block
31
+ @_content_for ||= {}
32
+ buf_was = @_out_buf
33
+ @_out_buf = ''
34
+ yield
35
+ @_content_for[key] = @_out_buf
36
+ @_out_buf = buf_was
37
+ elsif @_content_for
38
+ @_content_for[key]
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ register_plugin(:content_for, ContentFor)
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ require 'rack/csrf'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The csrf plugin adds CSRF protection using rack_csrf, along with
6
+ # some csrf helper methods to use in your views. To use it, load
7
+ # the plugin, with the options hash passed to Rack::Csrf:
8
+ #
9
+ # plugin :csrf, :raise=>true
10
+ #
11
+ # This adds the following instance methods:
12
+ #
13
+ # csrf_field :: The field name to use for the hidden/meta csrf tag.
14
+ # csrf_header :: The http header name to use for submitting csrf token via
15
+ # headers (useful for javascript).
16
+ # csrf_metatag :: An html meta tag string containing the token, suitable
17
+ # for placing in the page header
18
+ # csrf_tag :: An html hidden input tag string containing the token, suitable
19
+ # for placing in an html form.
20
+ # csrf_token :: The value of the csrf token, in case it needs to be accessed
21
+ # directly.
22
+ module Csrf
23
+ CSRF = ::Rack::Csrf
24
+
25
+ # Load the Rack::Csrf middleware into the app with the given options.
26
+ def self.configure(app, opts={})
27
+ app.use CSRF, opts
28
+ end
29
+
30
+ module InstanceMethods
31
+ # The name of the hidden/meta csrf tag.
32
+ def csrf_field
33
+ CSRF.field
34
+ end
35
+
36
+ # The http header name to use for submitting csrf token via headers.
37
+ def csrf_header
38
+ CSRF.header
39
+ end
40
+
41
+ # An html meta tag string containing the token.
42
+ def csrf_metatag(opts={})
43
+ CSRF.metatag(env, opts)
44
+ end
45
+
46
+ # An html hidden input tag string containing the token.
47
+ def csrf_tag
48
+ CSRF.tag(env)
49
+ end
50
+
51
+ # The value of the csrf token.
52
+ def csrf_token
53
+ CSRF.token(env)
54
+ end
55
+ end
56
+ end
57
+
58
+ register_plugin(:csrf, Csrf)
59
+ end
60
+ end
@@ -1,4 +1,4 @@
1
- require 'sinatra/flash/hash'
1
+ require 'delegate'
2
2
 
3
3
  class Roda
4
4
  module RodaPlugins
@@ -25,20 +25,66 @@ class Roda
25
25
  # end
26
26
  # end
27
27
  # end
28
- #
29
- # The flash plugin uses sinatra-flash internally, so you
30
- # must install sinatra-flash in order to use it.
31
28
  module Flash
32
- FlashHash = ::Sinatra::Flash::FlashHash
29
+ # Simple flash hash, where assiging to the hash updates the flash
30
+ # used in the following request.
31
+ class FlashHash < DelegateClass(Hash)
32
+ # The flash hash for the next request. This
33
+ # is what gets written to by #[]=.
34
+ attr_reader :next
35
+
36
+ # The flash hash for the current request
37
+ alias now __getobj__
38
+
39
+ # Setup the next hash when initializing, and handle treat nil
40
+ # as a new empty hash.
41
+ def initialize(hash={})
42
+ super(hash||{})
43
+ @next = {}
44
+ end
45
+
46
+ # Update the next hash with the given key and value.
47
+ def []=(k, v)
48
+ @next[k] = v
49
+ end
50
+
51
+ # Remove given key from the next hash, or clear the next hash if
52
+ # no argument is given.
53
+ def discard(key=(no_arg=true))
54
+ if no_arg
55
+ @next.clear
56
+ else
57
+ @next.delete(key)
58
+ end
59
+ end
60
+
61
+ # Copy the entry with the given key from the current hash to the
62
+ # next hash, or copy all entries from the current hash to the
63
+ # next hash if no argument is given.
64
+ def keep(key=(no_arg=true))
65
+ if no_arg
66
+ @next.merge!(self)
67
+ else
68
+ self[key] = self[key]
69
+ end
70
+ end
71
+
72
+ # Replace the current hash with the next hash and clear the next hash.
73
+ def sweep
74
+ replace(@next)
75
+ @next.clear
76
+ self
77
+ end
78
+ end
33
79
 
34
80
  module InstanceMethods
35
81
  # The internal session key used to store the flash.
36
- KEY = :flash
82
+ KEY = :_flash
37
83
 
38
84
  # Access the flash hash for the current request, loading
39
85
  # it from the session if it is not already loaded.
40
86
  def flash
41
- @_flash ||= FlashHash.new((session ? session[KEY] : {}))
87
+ @_flash ||= FlashHash.new(session[KEY])
42
88
  end
43
89
 
44
90
  private
@@ -1,18 +1,12 @@
1
1
  class Roda
2
2
  module RodaPlugins
3
- # The halt plugin augments the standard request +halt+ method to handle more than
4
- # just rack response arrays.
3
+ # The halt plugin augments the standard request +halt+ method to allow the response
4
+ # status, body, or headers to be changed when halting.
5
5
  #
6
6
  # After loading the halt plugin:
7
7
  #
8
8
  # plugin :halt
9
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
10
  # You can call the halt method with an integer to set the response status and return:
17
11
  #
18
12
  # route do |r|
@@ -22,23 +16,23 @@ class Roda
22
16
  # Or set the response body and return:
23
17
  #
24
18
  # route do |r|
25
- # r.halt("body')
19
+ # r.halt('body')
26
20
  # end
27
21
  #
28
22
  # Or set both:
29
23
  #
30
24
  # route do |r|
31
- # r.halt(403, "body')
25
+ # r.halt(403, 'body')
32
26
  # end
33
27
  #
34
28
  # Or set response status, headers, and body:
35
29
  #
36
30
  # route do |r|
37
- # r.halt(403, {'Content-Type'=>'text/csv'}, "body')
31
+ # r.halt(403, {'Content-Type'=>'text/csv'}, 'body')
38
32
  # end
39
33
  #
40
34
  # 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,
35
+ # arguments and providing them as a single rack response array. With a rack response array,
42
36
  # the values are used directly, while with 3 arguments, the headers given are merged into
43
37
  # the existing headers and the given body is written to the existing response body.
44
38
  module Halt
@@ -54,7 +48,7 @@ class Roda
54
48
  when String
55
49
  response.write v
56
50
  when Array
57
- super
51
+ throw :halt, v
58
52
  else
59
53
  raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
60
54
  end
@@ -69,7 +63,7 @@ class Roda
69
63
  raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
70
64
  end
71
65
 
72
- _halt response.finish
66
+ super()
73
67
  end
74
68
  end
75
69
  end
@@ -0,0 +1,56 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The head plugin attempts to automatically handle HEAD requests,
4
+ # by treating them as GET requests and returning an empty body
5
+ # without modifying the response status or response headers.
6
+ #
7
+ # So for the following routes,
8
+ #
9
+ # route do |r|
10
+ # r.root do
11
+ # 'root'
12
+ # end
13
+ #
14
+ # r.get 'a' do
15
+ # 'a'
16
+ # end
17
+ #
18
+ # r.is 'b', :method=>[:get, :post] do
19
+ # 'b'
20
+ # end
21
+ # end
22
+ #
23
+ # HEAD requests for +/+, +/a+, and +/b+ will all return 200 status
24
+ # with an empty body.
25
+ module Head
26
+ module InstanceMethods
27
+ # Always use an empty response body for head requests, with a
28
+ # content length of 0.
29
+ def call(*)
30
+ res = super
31
+ if request.head?
32
+ res[2] = []
33
+ end
34
+ res
35
+ end
36
+ end
37
+
38
+ module RequestMethods
39
+ # Consider HEAD requests as GET requests.
40
+ def is_get?
41
+ super || head?
42
+ end
43
+
44
+ private
45
+
46
+ # If the current request is a HEAD request, match if one of
47
+ # the given methods is a GET request.
48
+ def match_method(method)
49
+ super || (!method.is_a?(Array) && head? && method.to_s.upcase == 'GET')
50
+ end
51
+ end
52
+ end
53
+
54
+ register_plugin(:head, Head)
55
+ end
56
+ end