roda-cj 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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