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
@@ -35,14 +35,14 @@ class Roda
35
35
 
36
36
  # Match if the given mimetype is one of the accepted mimetypes.
37
37
  def match_accept(mimetype)
38
- if env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
38
+ if @env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
39
39
  response["Content-Type"] = mimetype
40
40
  end
41
41
  end
42
42
 
43
43
  # Match if the given uppercase key is present inside the environment.
44
44
  def match_header(key)
45
- env[key.upcase.tr("-","_")]
45
+ @env[key.upcase.tr("-","_")]
46
46
  end
47
47
 
48
48
  # Match if the host of the request is the same as the hostname.
@@ -0,0 +1,84 @@
1
+ require 'json'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The json plugin allows match blocks to return
6
+ # arrays or hashes, and have those arrays or hashes be
7
+ # converted to json which is used as the response body.
8
+ # It also sets the response content type to application/json.
9
+ # So you can take code like:
10
+ #
11
+ # r.root do
12
+ # response['Content-Type'] = 'application/json'
13
+ # [1, 2, 3].to_json
14
+ # end
15
+ # r.is "foo" do
16
+ # response['Content-Type'] = 'application/json'
17
+ # {'a'=>'b'}.to_json
18
+ # end
19
+ #
20
+ # and DRY it up:
21
+ #
22
+ # plugin :json
23
+ # r.root do
24
+ # [1, 2, 3]
25
+ # end
26
+ # r.is "foo" do
27
+ # {'a'=>'b'}
28
+ # end
29
+ #
30
+ # By default, only arrays and hashes are handled, but you
31
+ # can automatically convert other types to json by adding
32
+ # them to json_result_classes:
33
+ #
34
+ # plugin :json
35
+ # json_result_classes << Sequel::Model
36
+ module Json
37
+ # Set the classes to automatically convert to JSON
38
+ def self.configure(app)
39
+ app.instance_eval do
40
+ @json_result_classes ||= [Array, Hash]
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ # The classes that should be automatically converted to json
46
+ attr_reader :json_result_classes
47
+
48
+ # Copy the json_result_classes into the subclass
49
+ def inherited(subclass)
50
+ super
51
+ subclass.instance_variable_set(:@json_result_classes, json_result_classes.dup)
52
+ end
53
+ end
54
+
55
+ module RequestMethods
56
+ CONTENT_TYPE = 'Content-Type'.freeze
57
+ APPLICATION_JSON = 'application/json'.freeze
58
+
59
+ private
60
+
61
+ # If the result is an instance of one of the json_result_classes,
62
+ # convert the result to json and return it as the body, using the
63
+ # application/json content-type.
64
+ def block_result_body(result)
65
+ case result
66
+ when *self.class.roda_class.json_result_classes
67
+ response[CONTENT_TYPE] = APPLICATION_JSON
68
+ convert_to_json(result)
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ # Convert the given object to JSON. Uses to_json by default,
75
+ # but can be overridden to use a different implementation.
76
+ def convert_to_json(obj)
77
+ obj.to_json
78
+ end
79
+ end
80
+ end
81
+
82
+ register_plugin(:json, Json)
83
+ end
84
+ end
@@ -1,39 +1,56 @@
1
1
  class Roda
2
2
  module RodaPlugins
3
3
  # The multi_route plugin allows for multiple named routes, which the
4
- # main route block can dispatch to by name at any point. If the named
5
- # route doesn't handle the request, execution will continue, and if the
6
- # named route does handle the request, the response by the named route
7
- # will be returned.
4
+ # main route block can dispatch to by name at any point by calling +route+.
5
+ # If the named route doesn't handle the request, execution will continue,
6
+ # and if the named route does handle the request, the response returned by
7
+ # the named route will be returned.
8
+ #
9
+ # In addition, this also adds the +r.multi_route+ method, which will assume
10
+ # check if the first segment in the path matches a named route, and dispatch
11
+ # to that named route.
8
12
  #
9
13
  # Example:
10
14
  #
11
15
  # plugin :multi_route
12
16
  #
13
- # route(:foo) do |r|
17
+ # route('foo') do |r|
14
18
  # r.is 'bar' do
15
19
  # '/foo/bar'
16
20
  # end
17
21
  # end
18
22
  #
19
- # route(:bar) do |r|
23
+ # route('bar') do |r|
20
24
  # r.is 'foo' do
21
25
  # '/bar/foo'
22
26
  # end
23
27
  # end
24
28
  #
25
29
  # route do |r|
30
+ # r.multi_route
31
+ #
32
+ # # or
33
+ #
26
34
  # r.on "foo" do
27
- # route :foo
35
+ # r.route 'foo'
28
36
  # end
29
37
  #
30
38
  # r.on "bar" do
31
- # route :bar
39
+ # r.route 'bar'
32
40
  # end
33
41
  # end
34
42
  #
35
43
  # Note that in multi-threaded code, you should not attempt to add a
36
44
  # named route after accepting requests.
45
+ #
46
+ # If you want to use the +r.multi_route+ method, use string names for the
47
+ # named routes. Also, you can provide a block to +r.multi_route+ that is
48
+ # called if the route matches but the named route did not handle the
49
+ # request:
50
+ #
51
+ # r.multi_route do
52
+ # "default body"
53
+ # end
37
54
  module MultiRoute
38
55
  # Initialize storage for the named routes.
39
56
  def self.configure(app)
@@ -47,6 +64,11 @@ class Roda
47
64
  subclass.instance_variable_set(:@named_routes, @named_routes.dup)
48
65
  end
49
66
 
67
+ # An names for the currently stored named routes
68
+ def named_routes
69
+ @named_routes.keys
70
+ end
71
+
50
72
  # Return the named route with the given name.
51
73
  def named_route(name)
52
74
  @named_routes[name]
@@ -64,10 +86,28 @@ class Roda
64
86
  end
65
87
  end
66
88
 
67
- module InstanceMethods
89
+ module RequestClassMethods
90
+ # A regexp matching any of the current named routes.
91
+ def named_route_regexp
92
+ @named_route_regexp ||= /(#{Regexp.union(roda_class.named_routes)})/
93
+ end
94
+ end
95
+
96
+ module RequestMethods
97
+ # Check if the first segment in the path matches any of the current
98
+ # named routes. If so, call that named route. If not, do nothing.
99
+ # If the named route does not handle the request, and a block
100
+ # is given, yield to the block.
101
+ def multi_route
102
+ on self.class.named_route_regexp do |section|
103
+ route(section)
104
+ yield if block_given?
105
+ end
106
+ end
107
+
68
108
  # Dispatch to the named route with the given name.
69
109
  def route(name)
70
- instance_exec(request, &self.class.named_route(name))
110
+ scope.instance_exec(self, &self.class.roda_class.named_route(name))
71
111
  end
72
112
  end
73
113
  end
@@ -0,0 +1,140 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The not_allowed plugin makes Roda attempt to automatically
4
+ # support the 405 Method Not Allowed response status. The plugin
5
+ # changes the +r.get+ and +r.post+ verb methods to automatically
6
+ # return a 405 status if they are called with any arguments, and
7
+ # the arguments match but the request method does not match. So
8
+ # this code:
9
+ #
10
+ # r.get '' do
11
+ # "a"
12
+ # end
13
+ #
14
+ # will return a 200 response for <tt>GET /</tt> and a 405
15
+ # response for <tt>POST /</tt>.
16
+ #
17
+ # This plugin also changes the +r.is+ method so that if you use
18
+ # a verb method inside +r.is+, it returns a 405 status if none
19
+ # of the verb methods match. So this code:
20
+ #
21
+ # r.is '' do
22
+ # r.get do
23
+ # "a"
24
+ # end
25
+ #
26
+ # r.post do
27
+ # "b"
28
+ # end
29
+ # end
30
+ #
31
+ # will return a 200 response for <tt>GET /</tt> and <tt>POST /</tt>,
32
+ # but a 405 response for <tt>PUT /</tt>.
33
+ #
34
+ # Note that this plugin will probably not do what you want for
35
+ # code such as:
36
+ #
37
+ # r.get '' do
38
+ # "a"
39
+ # end
40
+ #
41
+ # r.post '' do
42
+ # "b"
43
+ # end
44
+ #
45
+ # Since for a <tt>POST /</tt> request, when +r.get+ method matches
46
+ # the path but not the request method, it will return an immediate
47
+ # 405 response. You must DRY up this code for it work correctly,
48
+ # like this:
49
+ #
50
+ # r.is '' do
51
+ # r.get do
52
+ # "a"
53
+ # end
54
+ #
55
+ # r.post do
56
+ # "b"
57
+ # end
58
+ # end
59
+ #
60
+ # In all cases where it uses a 405 response, it also sets the +Allow+
61
+ # header in the response to contain the request methods supported.
62
+ #
63
+ # To make this affect the verb methods added by the all_verbs plugin,
64
+ # load this plugin first.
65
+ module NotAllowed
66
+ # Redefine the +r.get+ and +r.post+ methods when loading the plugin.
67
+ def self.configure(app)
68
+ app.request_module do
69
+ app::RodaRequest.def_verb_method(self, :get)
70
+ app::RodaRequest.def_verb_method(self, :post)
71
+ end
72
+ end
73
+
74
+ module RequestClassMethods
75
+ # Define a method named +verb+ in the given module which will
76
+ # return a 405 response if the method is called with any
77
+ # arguments and the arguments terminally match but the
78
+ # request method does not.
79
+ #
80
+ # If called without any arguments, check to see if the call
81
+ # is inside a terminal match, and in that case record the
82
+ # request method used.
83
+ def def_verb_method(mod, verb)
84
+ mod.class_eval(<<-END, __FILE__, __LINE__+1)
85
+ def #{verb}(*args, &block)
86
+ if args.empty?
87
+ @_is_verbs << "#{verb.to_s.upcase}" if @_is_verbs
88
+ always(&block) if #{verb == :get ? :is_get : verb}?
89
+ else
90
+ args << ::Roda::RodaPlugins::Base::RequestMethods::TERM
91
+ if_match(args) do |*args|
92
+ if #{verb == :get ? :is_get : verb}?
93
+ block_result(yield(*args))
94
+ throw :halt, response.finish
95
+ end
96
+ response.status = 405
97
+ response['Allow'] = '#{verb.to_s.upcase}'
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ END
103
+ end
104
+ end
105
+
106
+ module RequestMethods
107
+ # Keep track of verb calls inside the block. If there are any
108
+ # verb calls inside the block, but the block returned, assume
109
+ # that the verb calls inside the block did not match, and
110
+ # since there was already a successful terminal match, the
111
+ # request method must not be allowed, so return a 405
112
+ # response in that case.
113
+ def is(*verbs)
114
+ super(*verbs) do
115
+ begin
116
+ @_is_verbs = []
117
+
118
+ ret = if verbs.empty?
119
+ yield
120
+ else
121
+ yield(*captures)
122
+ end
123
+
124
+ unless @_is_verbs.empty?
125
+ response.status = 405
126
+ response['Allow'] = @_is_verbs.join(', ')
127
+ end
128
+
129
+ ret
130
+ ensure
131
+ @_is_verbs = nil
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ register_plugin(:not_allowed, NotAllowed)
139
+ end
140
+ end
@@ -1,6 +1,6 @@
1
1
  class Roda
2
2
  module RodaPlugins
3
- # The pass plugin adds a request +pass+ method to skip the current +on+
3
+ # The pass plugin adds a request +pass+ method to skip the current match
4
4
  # block as if it did not match.
5
5
  #
6
6
  # plugin :pass
@@ -17,14 +17,21 @@ class Roda
17
17
  # end
18
18
  module Pass
19
19
  module RequestMethods
20
- # Handle passing inside the current block.
21
- def on(*)
20
+ # Skip the current match block as if it did not match.
21
+ def pass
22
+ throw :pass
23
+ end
24
+
25
+ private
26
+
27
+ # Handle passing inside the match block.
28
+ def always
22
29
  catch(:pass){super}
23
30
  end
24
31
 
25
- # Skip the current #on block as if it did not match.
26
- def pass
27
- throw :pass
32
+ # Handle passing inside the match block.
33
+ def if_match(_)
34
+ catch(:pass){super}
28
35
  end
29
36
  end
30
37
  end
@@ -0,0 +1,70 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The per_thread_caching plugin changes the default cache
4
+ # from being a shared thread safe cache to a separate cache per
5
+ # thread. This means getting or setting values no longer
6
+ # needs a mutex on non-MRI ruby implementations, which may be
7
+ # faster when using a thread pool. However, since the caches
8
+ # are no longer shared, this will take up more memory.
9
+ #
10
+ # Note that it does not make sense to use this plugin on MRI,
11
+ # since the default cache on MRI doesn't use a mutex as it
12
+ # is already thread safe due to the GVL.
13
+ #
14
+ # Using this plugin changes the matcher regexp cache to use
15
+ # per-thread caches, and changes the default for future
16
+ # thread-safe caches to use per-thread caches.
17
+ #
18
+ # If you want the render plugin's template cache to use
19
+ # per-thread caches, you should load this plugin before the
20
+ # render plugin.
21
+ module PerThreadCaching
22
+ def self.configure(app)
23
+ app::RodaRequest.match_pattern_cache = app.thread_safe_cache
24
+ end
25
+
26
+ class Cache
27
+ # Mutex used to ensure multiple per-thread caches
28
+ # don't use the same key
29
+ MUTEX = ::Mutex.new
30
+
31
+ n = 0
32
+ # Auto incrementing number proc used to make sure
33
+ # multiple thread-thread caches don't use the same key.
34
+ N = lambda{MUTEX.synchronize{n += 1}}
35
+
36
+ # Store unique symbol used to look up in the per
37
+ # thread caches.
38
+ def initialize
39
+ @o = :"roda_per_thread_cache_#{N.call}"
40
+ end
41
+
42
+ # Return the current thread's cached value.
43
+ def [](key)
44
+ _hash[key]
45
+ end
46
+
47
+ # Set the current thread's cached value.
48
+ def []=(key, value)
49
+ _hash[key] = value
50
+ end
51
+
52
+ private
53
+
54
+ # The current thread's cache.
55
+ def _hash
56
+ ::Thread.current[@o] ||= {}
57
+ end
58
+ end
59
+
60
+ module ClassMethods
61
+ # Use the per-thread cache instead of the default cache.
62
+ def thread_safe_cache
63
+ Cache.new
64
+ end
65
+ end
66
+ end
67
+
68
+ register_plugin(:per_thread_caching, PerThreadCaching)
69
+ end
70
+ end