roda 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +34 -0
  3. data/README.rdoc +18 -13
  4. data/Rakefile +8 -0
  5. data/doc/conventions.rdoc +163 -0
  6. data/doc/release_notes/1.1.0.txt +226 -0
  7. data/lib/roda.rb +51 -22
  8. data/lib/roda/plugins/assets.rb +613 -0
  9. data/lib/roda/plugins/caching.rb +215 -0
  10. data/lib/roda/plugins/chunked.rb +278 -0
  11. data/lib/roda/plugins/error_email.rb +112 -0
  12. data/lib/roda/plugins/flash.rb +3 -3
  13. data/lib/roda/plugins/hooks.rb +1 -1
  14. data/lib/roda/plugins/indifferent_params.rb +3 -3
  15. data/lib/roda/plugins/middleware.rb +3 -8
  16. data/lib/roda/plugins/multi_route.rb +110 -18
  17. data/lib/roda/plugins/not_allowed.rb +3 -3
  18. data/lib/roda/plugins/path.rb +38 -0
  19. data/lib/roda/plugins/render.rb +18 -16
  20. data/lib/roda/plugins/render_each.rb +0 -2
  21. data/lib/roda/plugins/streaming.rb +1 -2
  22. data/lib/roda/plugins/view_subdirs.rb +7 -1
  23. data/lib/roda/version.rb +1 -1
  24. data/spec/assets/css/app.scss +1 -0
  25. data/spec/assets/css/no_access.css +1 -0
  26. data/spec/assets/css/raw.css +1 -0
  27. data/spec/assets/js/head/app.js +1 -0
  28. data/spec/integration_spec.rb +95 -3
  29. data/spec/matchers_spec.rb +2 -2
  30. data/spec/plugin/assets_spec.rb +413 -0
  31. data/spec/plugin/caching_spec.rb +335 -0
  32. data/spec/plugin/chunked_spec.rb +182 -0
  33. data/spec/plugin/default_headers_spec.rb +6 -5
  34. data/spec/plugin/error_email_spec.rb +76 -0
  35. data/spec/plugin/multi_route_spec.rb +120 -0
  36. data/spec/plugin/not_allowed_spec.rb +14 -3
  37. data/spec/plugin/path_spec.rb +29 -0
  38. data/spec/plugin/render_each_spec.rb +6 -1
  39. data/spec/plugin/symbol_matchers_spec.rb +7 -2
  40. data/spec/request_spec.rb +10 -0
  41. data/spec/response_spec.rb +47 -0
  42. data/spec/views/about.erb +1 -0
  43. data/spec/views/about.str +1 -0
  44. data/spec/views/content-yield.erb +1 -0
  45. data/spec/views/home.erb +2 -0
  46. data/spec/views/home.str +2 -0
  47. data/spec/views/layout-alternative.erb +2 -0
  48. data/spec/views/layout-yield.erb +3 -0
  49. data/spec/views/layout.erb +2 -0
  50. data/spec/views/layout.str +2 -0
  51. metadata +57 -2
@@ -0,0 +1,112 @@
1
+ require 'net/smtp'
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The error_email plugin adds an +error_email+ instance method that
6
+ # send an email related to the exception. This is most useful if you are
7
+ # also using the error_handler plugin:
8
+ #
9
+ # plugin :error_email, :to=>'to@example.com', :from=>'from@example.com'
10
+ # plugin :error_handler do |e|
11
+ # error_email(e)
12
+ # 'Internal Server Error'
13
+ # end
14
+ #
15
+ # Options:
16
+ #
17
+ # :from :: The From address to use in the email (required)
18
+ # :headers :: A hash of additional headers to use in the email (default: empty hash)
19
+ # :host :: The SMTP server to use to send the email (default: localhost)
20
+ # :prefix :: A prefix to use in the email's subject line (default: no prefix)
21
+ # :to :: The To address to use in the email (required)
22
+ #
23
+ # The subject of the error email shows the exception class and message.
24
+ # The body of the error email shows the backtrace of the error and the
25
+ # request environment, as well the request params and session variables (if any).
26
+ #
27
+ # Note that emailing on every error as shown above is only appropriate
28
+ # for low traffic web applications. For high traffic web applications,
29
+ # use an error reporting service instead of this plugin.
30
+ module ErrorEmail
31
+ DEFAULTS = {
32
+ :headers=>{},
33
+ :host=>'localhost',
34
+ # :nocov:
35
+ :emailer=>lambda{|h| Net::SMTP.start(h[:host]){|s| s.send_message(h[:message], h[:from], h[:to])}},
36
+ # :nocov:
37
+ :default_headers=>lambda do |h, e|
38
+ {'From'=>h[:from], 'To'=>h[:to], 'Subject'=>"#{h[:prefix]}#{e.class}: #{e.message}"}
39
+ end,
40
+ :body=>lambda do |s, e|
41
+ format = lambda{|h| h.map{|k, v| "#{k.inspect} => #{v.inspect}"}.sort.join("\n")}
42
+
43
+ message = <<END
44
+ Path: #{s.request.full_path_info}
45
+
46
+ Backtrace:
47
+
48
+ #{e.backtrace.join("\n")}
49
+
50
+ ENV:
51
+
52
+ #{format[s.env]}
53
+ END
54
+ unless s.request.params.empty?
55
+ message << <<END
56
+
57
+ Params:
58
+
59
+ #{format[s.request.params]}
60
+ END
61
+ end
62
+
63
+ if s.env['rack.session']
64
+ message << <<END
65
+
66
+ Session:
67
+
68
+ #{format[s.session]}
69
+ END
70
+ end
71
+
72
+ message
73
+ end
74
+ }
75
+
76
+ # Set default opts for plugin. See ErrorEmail module RDoc for options.
77
+ def self.configure(app, opts={})
78
+ email_opts = app.opts[:error_email] ||= DEFAULTS
79
+ email_opts = email_opts.merge(opts)
80
+ unless email_opts[:to] && email_opts[:from]
81
+ raise RodaError, "must provide :to and :from options to error_email plugin"
82
+ end
83
+ app.opts[:error_email] = email_opts
84
+ end
85
+
86
+ module ClassMethods
87
+ # Dup the error email opts in the subclass so changes to the subclass do not affect
88
+ # the superclass.
89
+ def inherited(subclass)
90
+ super
91
+ subclass.opts[:error_email] = subclass.opts[:error_email].dup
92
+ subclass.opts[:error_email][:headers] = subclass.opts[:error_email][:headers].dup
93
+ end
94
+ end
95
+
96
+ module InstanceMethods
97
+ # Send an email for the given error.
98
+ def error_email(e)
99
+ email_opts = self.class.opts[:error_email].dup
100
+ headers = email_opts[:default_headers].call(email_opts, e)
101
+ headers = headers.merge(email_opts[:headers])
102
+ headers = headers.map{|k,v| "#{k}: #{v.gsub(/\r?\n/m, "\r\n ")}"}.sort.join("\r\n")
103
+ body = email_opts[:body].call(self, e)
104
+ email_opts[:message] = "#{headers}\r\n\r\n#{body}"
105
+ email_opts[:emailer].call(email_opts)
106
+ end
107
+ end
108
+ end
109
+
110
+ register_plugin(:error_email, ErrorEmail)
111
+ end
112
+ end
@@ -26,6 +26,9 @@ class Roda
26
26
  # end
27
27
  # end
28
28
  module Flash
29
+ # The internal session key used to store the flash.
30
+ KEY = :_flash
31
+
29
32
  # Simple flash hash, where assiging to the hash updates the flash
30
33
  # used in the following request.
31
34
  class FlashHash < DelegateClass(Hash)
@@ -78,9 +81,6 @@ class Roda
78
81
  end
79
82
 
80
83
  module InstanceMethods
81
- # The internal session key used to store the flash.
82
- KEY = :_flash
83
-
84
84
  # Access the flash hash for the current request, loading
85
85
  # it from the session if it is not already loaded.
86
86
  def flash
@@ -30,7 +30,7 @@ class Roda
30
30
  # handle cases where before hooks are added after the route block.
31
31
  module Hooks
32
32
  def self.configure(app)
33
- @app.instance_exec do
33
+ app.instance_exec do
34
34
  @after ||= nil
35
35
  @before ||= nil
36
36
  end
@@ -30,9 +30,9 @@ class Roda
30
30
  def indifferent_params(params)
31
31
  case params
32
32
  when Hash
33
- h = Hash.new{|h, k| h[k.to_s] if Symbol === k}
34
- params.each{|k, v| h[k] = indifferent_params(v)}
35
- h
33
+ hash = Hash.new{|h, k| h[k.to_s] if Symbol === k}
34
+ params.each{|k, v| hash[k] = indifferent_params(v)}
35
+ hash
36
36
  when Array
37
37
  params.map{|x| indifferent_params(x)}
38
38
  else
@@ -62,14 +62,9 @@ class Roda
62
62
  end
63
63
 
64
64
  module ClassMethods
65
- # If an argument is given, this is a middleware app, so create a Forwarder.
66
- # Otherwise, this is a usual instance creation, so call super.
67
- def new(app=nil)
68
- if app
69
- Forwarder.new(self, app)
70
- else
71
- super()
72
- end
65
+ # Create a Forwarder instead of a new instance.
66
+ def new(app)
67
+ Forwarder.new(self, app)
73
68
  end
74
69
 
75
70
  # Override the route block so that if no route matches, we throw so
@@ -51,35 +51,113 @@ class Roda
51
51
  # r.multi_route do
52
52
  # "default body"
53
53
  # end
54
+ #
55
+ # If a block is not provided to multi_route, the return value of the named
56
+ # route block will be used.
57
+ #
58
+ # == Routing Files
59
+ #
60
+ # The convention when using the multi_route plugin is to have a single
61
+ # named route per file, and these routing files should be stored in
62
+ # a routes subdirectory in your application. So for the above example, you
63
+ # would use the following files:
64
+ #
65
+ # routes/bar.rb
66
+ # routes/foo.rb
67
+ #
68
+ # == Namespace Support
69
+ #
70
+ # The multi_route plugin also has support for namespaces, allowing you to
71
+ # use r.multi_route at multiple levels in your routing tree. Example:
72
+ #
73
+ # route('foo') do |r|
74
+ # r.multi_route('foo')
75
+ # end
76
+ #
77
+ # route('bar') do |r|
78
+ # r.multi_route('bar')
79
+ # end
80
+ #
81
+ # route('baz', 'foo') do |r|
82
+ # # handles /foo/baz prefix
83
+ # end
84
+ #
85
+ # route('quux', 'foo') do |r|
86
+ # # handles /foo/quux prefix
87
+ # end
88
+ #
89
+ # route('baz', 'bar') do |r|
90
+ # # handles /bar/baz prefix
91
+ # end
92
+ #
93
+ # route('quux', 'bar') do |r|
94
+ # # handles /bar/quux prefix
95
+ # end
96
+ #
97
+ # route do |r|
98
+ # r.multi_route
99
+ #
100
+ # # or
101
+ #
102
+ # r.on "foo" do
103
+ # r.on("baz"){r.route("baz", "foo")}
104
+ # r.on("quux"){r.route("quux", "foo")}
105
+ # end
106
+ #
107
+ # r.on "bar" do
108
+ # r.on("baz"){r.route("baz", "bar")}
109
+ # r.on("quux"){r.route("quux", "bar")}
110
+ # end
111
+ # end
112
+ #
113
+ # === Routing Files
114
+ #
115
+ # The convention when using namespaces with the multi_route plugin is to
116
+ # store the routing files in subdirectories per namespace. So for the
117
+ # above example, you would have the following routing files:
118
+ #
119
+ # routes/bar.rb
120
+ # routes/bar/baz.rb
121
+ # routes/bar/quux.rb
122
+ # routes/foo.rb
123
+ # routes/foo/baz.rb
124
+ # routes/foo/quux.rb
54
125
  module MultiRoute
55
126
  # Initialize storage for the named routes.
56
127
  def self.configure(app)
57
- app.instance_exec{@named_routes ||= {}}
128
+ app.instance_exec do
129
+ @namespaced_routes ||= {}
130
+ app::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
131
+ end
58
132
  end
59
133
 
60
134
  module ClassMethods
61
135
  # Copy the named routes into the subclass when inheriting.
62
136
  def inherited(subclass)
63
137
  super
64
- subclass.instance_variable_set(:@named_routes, @named_routes.dup)
138
+ nsr = subclass.instance_variable_set(:@namespaced_routes, {})
139
+ @namespaced_routes.each{|k, v| nsr[k] = v.dup}
140
+ subclass::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {})
65
141
  end
66
142
 
67
- # An names for the currently stored named routes
68
- def named_routes
69
- @named_routes.keys
143
+ # The names for the currently stored named routes
144
+ def named_routes(namespace=nil)
145
+ @namespaced_routes[namespace].keys
70
146
  end
71
147
 
72
148
  # Return the named route with the given name.
73
- def named_route(name)
74
- @named_routes[name]
149
+ def named_route(name, namespace=nil)
150
+ @namespaced_routes[namespace][name]
75
151
  end
76
152
 
77
- # If the given route has a named, treat it as a named route and
153
+ # If the given route has a name, treat it as a named route and
78
154
  # store the route block. Otherwise, this is the main route, so
79
155
  # call super.
80
- def route(name=nil, &block)
156
+ def route(name=nil, namespace=nil, &block)
81
157
  if name
82
- @named_routes[name] = block
158
+ @namespaced_routes[namespace] ||= {}
159
+ @namespaced_routes[namespace][name] = block
160
+ self::RodaRequest.clear_named_route_regexp!(namespace)
83
161
  else
84
162
  super(&block)
85
163
  end
@@ -87,9 +165,19 @@ class Roda
87
165
  end
88
166
 
89
167
  module RequestClassMethods
168
+ # Clear cached regexp for named routes, it will be regenerated
169
+ # the next time it is needed.
170
+ #
171
+ # This shouldn't be an issue in production applications, but
172
+ # during development it's useful to support new named routes
173
+ # being added while the application is running.
174
+ def clear_named_route_regexp!(namespace=nil)
175
+ @namespaced_route_regexps.delete(namespace)
176
+ end
177
+
90
178
  # A regexp matching any of the current named routes.
91
- def named_route_regexp
92
- @named_route_regexp ||= /(#{Regexp.union(roda_class.named_routes)})/
179
+ def named_route_regexp(namespace=nil)
180
+ @namespaced_route_regexps[namespace] ||= /(#{Regexp.union(roda_class.named_routes(namespace).select{|s| s.is_a?(String)}.sort.reverse)})/
93
181
  end
94
182
  end
95
183
 
@@ -98,16 +186,20 @@ class Roda
98
186
  # named routes. If so, call that named route. If not, do nothing.
99
187
  # If the named route does not handle the request, and a block
100
188
  # 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?
189
+ def multi_route(namespace=nil)
190
+ on self.class.named_route_regexp(namespace) do |section|
191
+ r = route(section, namespace)
192
+ if block_given?
193
+ yield
194
+ else
195
+ r
196
+ end
105
197
  end
106
198
  end
107
199
 
108
200
  # Dispatch to the named route with the given name.
109
- def route(name)
110
- scope.instance_exec(self, &self.class.roda_class.named_route(name))
201
+ def route(name, namespace=nil)
202
+ scope.instance_exec(self, &self.class.roda_class.named_route(name, namespace))
111
203
  end
112
204
  end
113
205
  end
@@ -84,13 +84,13 @@ class Roda
84
84
  mod.class_eval(<<-END, __FILE__, __LINE__+1)
85
85
  def #{verb}(*args, &block)
86
86
  if args.empty?
87
- @_is_verbs << "#{verb.to_s.upcase}" if @_is_verbs
87
+ @_is_verbs << "#{verb.to_s.upcase}" if defined?(@_is_verbs)
88
88
  always(&block) if #{verb == :get ? :is_get : verb}?
89
89
  else
90
90
  args << ::Roda::RodaPlugins::Base::RequestMethods::TERM
91
- if_match(args) do |*args|
91
+ if_match(args) do |*a|
92
92
  if #{verb == :get ? :is_get : verb}?
93
- block_result(yield(*args))
93
+ block_result(yield(*a))
94
94
  throw :halt, response.finish
95
95
  end
96
96
  response.status = 405
@@ -0,0 +1,38 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The path plugin adds support for named paths. Using the +path+ class method, you can
4
+ # easily create <tt>*_path</tt> instance methods for each named path. Those instance
5
+ # methods can then be called if you need to get the path for a form action, link,
6
+ # redirect, or anything else. Example:
7
+ #
8
+ # plugin :path
9
+ # path :foo, '/foo'
10
+ # path :bar do |bar|
11
+ # "/bar/#{bar.id}"
12
+ # end
13
+ #
14
+ # route do |r|
15
+ # r.post 'bar' do
16
+ # bar = Bar.create(r.params['bar'])
17
+ # r.redirect bar_path(bar)
18
+ # end
19
+ # end
20
+ module Path
21
+ module ClassMethods
22
+ def path(name, path=nil, &block)
23
+ raise RodaError, "cannot provide both path and block to Roda.path" if path && block
24
+ raise RodaError, "must provide either path or block to Roda.path" unless path || block
25
+
26
+ if path
27
+ path = path.dup.freeze
28
+ block = lambda{path}
29
+ end
30
+
31
+ define_method("#{name}_path", &block)
32
+ end
33
+ end
34
+ end
35
+
36
+ register_plugin(:path, Path)
37
+ end
38
+ end
@@ -18,20 +18,18 @@ class Roda
18
18
  # end
19
19
  # end
20
20
  #
21
- # You can provide options to the plugin method, or later by modifying
22
- # +render_opts+.
21
+ # You can provide options to the plugin method:
23
22
  #
24
- # plugin :render, :engine=>'haml'
25
- #
26
- # render_opts[:views] = 'admin_views'
23
+ # plugin :render, :engine=>'haml', :views=>'admin_views'
27
24
  #
28
25
  # The following options are supported:
29
26
  #
30
27
  # :cache :: nil/false to not cache templates (useful for development), defaults
31
28
  # to true to automatically use the default template cache.
32
29
  # :engine :: The tilt engine to use for rendering, defaults to 'erb'.
33
- # :escape :: Use Roda's Erubis escaping support, which handles postfix
34
- # conditions inside <%= %> tags.
30
+ # :escape :: Use Roda's Erubis escaping support, which makes <%= %> escape output,
31
+ # <%== %> not escape output, and handles postfix conditions inside
32
+ # <%= %> tags.
35
33
  # :ext :: The file extension to assume for view files, defaults to the :engine
36
34
  # option.
37
35
  # :layout :: The base name of the layout file, defaults to 'layout'.
@@ -69,23 +67,25 @@ class Roda
69
67
  # If you pass a hash as the first argument to +view+ or +render+, it should
70
68
  # have either +:inline+ or +:path+ as one of the keys.
71
69
  module Render
72
- def self.load_dependencies(app, opts={})
70
+ OPTS={}.freeze
71
+
72
+ def self.load_dependencies(app, opts=OPTS)
73
73
  if opts[:escape]
74
74
  app.plugin :_erubis_escaping
75
75
  end
76
76
  end
77
77
 
78
78
  # Setup default rendering options. See Render for details.
79
- def self.configure(app, opts={})
79
+ def self.configure(app, opts=OPTS)
80
80
  if app.opts[:render]
81
81
  app.opts[:render].merge!(opts)
82
- else
82
+ else
83
83
  app.opts[:render] = opts.dup
84
84
  end
85
85
 
86
86
  opts = app.opts[:render]
87
87
  opts[:engine] ||= "erb"
88
- opts[:ext] = nil unless opts.has_key?(:ext)
88
+ opts[:ext] = nil unless opts.has_key?(:ext)
89
89
  opts[:views] ||= File.expand_path("views", Dir.pwd)
90
90
  opts[:layout] = "layout" unless opts.has_key?(:layout)
91
91
  opts[:layout_opts] ||= (opts[:layout_opts] || {}).dup
@@ -120,7 +120,7 @@ class Roda
120
120
 
121
121
  module InstanceMethods
122
122
  # Render the given template. See Render for details.
123
- def render(template, opts = {}, &block)
123
+ def render(template, opts = OPTS, &block)
124
124
  if template.is_a?(Hash)
125
125
  if opts.empty?
126
126
  opts = template
@@ -143,10 +143,12 @@ class Roda
143
143
 
144
144
  cached_template(path) do
145
145
  template_class.new(path, 1, render_opts[:opts].merge(opts), &template_block)
146
- end.render(self, opts[:locals], &block)
146
+ end.render(self, (opts[:locals]||OPTS), &block)
147
147
  end
148
148
 
149
- # Return the render options for the instance's class.
149
+ # Return the render options for the instance's class. While this
150
+ # is not currently frozen, it may be frozen in a future version,
151
+ # so you should not attempt to modify it.
150
152
  def render_opts
151
153
  self.class.render_opts
152
154
  end
@@ -154,7 +156,7 @@ class Roda
154
156
  # Render the given template. If there is a default layout
155
157
  # for the class, take the result of the template rendering
156
158
  # and render it inside the layout. See Render for details.
157
- def view(template, opts={})
159
+ def view(template, opts=OPTS)
158
160
  if template.is_a?(Hash)
159
161
  if opts.empty?
160
162
  opts = template
@@ -170,7 +172,7 @@ class Roda
170
172
  layout_opts = render_opts[:layout_opts].merge(layout_opts)
171
173
  end
172
174
 
173
- content = render(layout, layout_opts||{}){content}
175
+ content = render(layout, layout_opts||OPTS){content}
174
176
  end
175
177
 
176
178
  content