roda-cj 0.9.2 → 0.9.3

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.
@@ -35,7 +35,7 @@ class Roda
35
35
  if ::Rack::Request.method_defined?("#{t}?")
36
36
  class_eval(<<-END, __FILE__, __LINE__+1)
37
37
  def #{t}(*args, &block)
38
- is_or_on(*args, &block) if #{t}?
38
+ _verb(args, &block) if #{t}?
39
39
  end
40
40
  END
41
41
  end
@@ -0,0 +1,91 @@
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
+
39
+ script = env[SCRIPT_NAME]
40
+ path = env[PATH_INFO]
41
+ caps = captures.dup
42
+ arg.each do |v|
43
+ if match(v, rest)
44
+ if v.is_a?(String)
45
+ captures.push(v)
46
+ end
47
+
48
+ if match_all(rest)
49
+ return true
50
+ end
51
+
52
+ # Matching all remaining elements failed, reset state
53
+ captures.replace(caps)
54
+ env[SCRIPT_NAME] = script
55
+ env[PATH_INFO] = path
56
+ end
57
+ end
58
+ false
59
+ end
60
+
61
+ # If any of the args are an array, handle backtracking such
62
+ # that if a later matcher fails, we roll back to the current
63
+ # matcher and proceed to the next entry in the array.
64
+ def match_all(args)
65
+ args = args.dup
66
+ until args.empty?
67
+ arg = args.shift
68
+ if match(arg, args)
69
+ return true if arg.is_a?(Array)
70
+ else
71
+ return
72
+ end
73
+ end
74
+ true
75
+ end
76
+
77
+ # When matching an array, include the remaining arguments,
78
+ # otherwise, just match the single argument.
79
+ def match(v, rest = nil)
80
+ if v.is_a?(Array)
81
+ _match_array(v, rest)
82
+ else
83
+ super(v)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ register_plugin(:backtracking_array, BacktrackingArray)
90
+ end
91
+ 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
@@ -54,7 +54,7 @@ class Roda
54
54
  when String
55
55
  response.write v
56
56
  when Array
57
- super
57
+ throw :halt, v
58
58
  else
59
59
  raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
60
60
  end
@@ -69,7 +69,7 @@ class Roda
69
69
  raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
70
70
  end
71
71
 
72
- _halt response.finish
72
+ super()
73
73
  end
74
74
  end
75
75
  end
@@ -0,0 +1,84 @@
1
+ require 'json'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The json plugin allows matching 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
@@ -18,7 +18,7 @@ class Roda
18
18
  module Pass
19
19
  module RequestMethods
20
20
  # Handle passing inside the current block.
21
- def on(*)
21
+ def _on(_)
22
22
  catch(:pass){super}
23
23
  end
24
24
 
@@ -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
@@ -27,9 +27,8 @@ class Roda
27
27
  #
28
28
  # The following options are supported:
29
29
  #
30
- # :cache :: A specific cache to store templates in, or nil/false to not
31
- # cache templates (useful for development), defaults to true to
32
- # automatically use the default template cache.
30
+ # :cache :: nil/false to not cache templates (useful for development), defaults
31
+ # to true to automatically use the default template cache.
33
32
  # :engine :: The tilt engine to use for rendering, defaults to 'erb'.
34
33
  # :ext :: The file extension to assume for view files, defaults to the :engine
35
34
  # option.
@@ -65,34 +64,6 @@ class Roda
65
64
  # If you pass a hash as the first argument to +view+ or +render+, it should
66
65
  # have either +:inline+ or +:path+ as one of the keys.
67
66
  module Render
68
- # Default template cache. Thread-safe so that multiple threads can
69
- # simultaneously use the cache.
70
- class Cache
71
- # Mutex used to synchronize access to the cache. Uses a
72
- # singleton mutex to reduce memory.
73
- MUTEX = ::Mutex.new
74
-
75
- # Initialize the cache.
76
- def initialize
77
- MUTEX.synchronize{@cache = {}}
78
- end
79
-
80
- # Clear the cache.
81
- alias clear initialize
82
-
83
- # If the template is found in the cache under the given key,
84
- # return it, otherwise yield to get the template, and
85
- # store the template under the given key
86
- def fetch(key)
87
- unless template = MUTEX.synchronize{@cache[key]}
88
- template = yield
89
- MUTEX.synchronize{@cache[key] = template}
90
- end
91
-
92
- template
93
- end
94
- end
95
-
96
67
  # Setup default rendering options. See Render for details.
97
68
  def self.configure(app, opts={})
98
69
  if app.opts[:render]
@@ -112,8 +83,7 @@ class Roda
112
83
  if RUBY_VERSION >= "1.9"
113
84
  opts[:opts][:default_encoding] ||= Encoding.default_external
114
85
  end
115
- cache = opts.fetch(:cache, true)
116
- opts[:cache] = Cache.new if cache == true
86
+ opts[:cache] = app.thread_safe_cache if opts.fetch(:cache, true)
117
87
  end
118
88
 
119
89
  module ClassMethods
@@ -125,7 +95,7 @@ class Roda
125
95
  opts = subclass.opts[:render] = render_opts.dup
126
96
  opts[:layout_opts] = opts[:layout_opts].dup
127
97
  opts[:opts] = opts[:opts].dup
128
- opts[:cache] = Cache.new if opts[:cache]
98
+ opts[:cache] = thread_safe_cache if opts[:cache]
129
99
  end
130
100
 
131
101
  # Return the render options for this class.
@@ -198,7 +168,10 @@ class Roda
198
168
  # to get the template.
199
169
  def cached_template(path, &block)
200
170
  if cache = render_opts[:cache]
201
- cache.fetch(path, &block)
171
+ unless template = cache[path]
172
+ template = cache[path] = yield
173
+ end
174
+ template
202
175
  else
203
176
  yield
204
177
  end
@@ -0,0 +1,75 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The symbol_matchers plugin allows you do define custom regexps to use
4
+ # for specific symbols. For example, if you have a route such as:
5
+ #
6
+ # r.on :username do
7
+ # # ...
8
+ # end
9
+ #
10
+ # By default this will match all segments. However, if your usernames
11
+ # must be 6-20 characters, and can only contain +a-z+ and +0-9+, you can do:
12
+ #
13
+ # plugin :symbol_matchers
14
+ # symbol_matcher :username, /([a-z0-9]{6,20})/
15
+ #
16
+ # Then the route will only if the path is +/foobar123+, but not if it is
17
+ # +/foo+, +/FooBar123+, or +/foobar_123+.
18
+ #
19
+ # Note that this feature does not apply to just symbols, but also to
20
+ # embedded colons in strings, so the following:
21
+ #
22
+ # r.on "users/:username" do
23
+ # # ...
24
+ # end
25
+ #
26
+ # Would match +/users/foobar123+, but not +/users/foo+, +/users/FooBar123+,
27
+ # or +/users/foobar_123+.
28
+ #
29
+ # By default, this plugin sets up the following symbol matchers:
30
+ #
31
+ # :d :: <tt>/\d+/</tt>, a decimal segment
32
+ # :format :: <tt>/(?:\.(\w+))?/</tt>, an optional format
33
+ # :opt :: <tt>/(?:\/([^\/]+))?</tt>, an optional segment
34
+ # :optd :: <tt>/(?:\/(\d+))?</tt>, an optional decimal segment
35
+ # :w :: <tt>/\w+/</tt>, a alphanumeric segment
36
+ #
37
+ # Note that because of how segment matching works, :format, :opt, and :optd
38
+ # are only going to work inside of a string, like this:
39
+ #
40
+ # r.is "album:opt" do |id|
41
+ # # matches /album (yielding nil) and /album/foo (yielding "foo")
42
+ # # does not match /album/ or /album/foo/bar
43
+ module SymbolMatchers
44
+ def self.configure(app)
45
+ app.symbol_matcher(:d, /(\d+)/)
46
+ app.symbol_matcher(:format, /(?:\.(\w+))?/)
47
+ app.symbol_matcher(:opt, /(?:\/([^\/]+))?/)
48
+ app.symbol_matcher(:optd, /(?:\/(\d+))?/)
49
+ app.symbol_matcher(:w, /(\w+)/)
50
+ end
51
+
52
+ module ClassMethods
53
+ # Set the regexp to use for the given symbol, instead of the default.
54
+ def symbol_matcher(s, re)
55
+ request_module{define_method(:"match_symbol_#{s}"){re}}
56
+ end
57
+ end
58
+
59
+ module RequestMethods
60
+ # Allow for symbol specific regexps, by using match_symbol_#{s} if
61
+ # defined. If not defined, calls super for the default behavior.
62
+ def _match_symbol_regexp(s)
63
+ meth = :"match_symbol_#{s}"
64
+ if respond_to?(meth)
65
+ send(meth)
66
+ else
67
+ super
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ register_plugin(:symbol_matchers, SymbolMatchers)
74
+ end
75
+ end