roda-cj 0.9.6 → 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.
- data/CHANGELOG +16 -0
- data/README.rdoc +211 -103
- data/Rakefile +1 -1
- data/doc/release_notes/1.0.0.txt +329 -0
- data/lib/roda.rb +295 -42
- data/lib/roda/plugins/all_verbs.rb +1 -1
- data/lib/roda/plugins/assets.rb +277 -0
- data/lib/roda/plugins/backtracking_array.rb +1 -1
- data/lib/roda/plugins/error_email.rb +110 -0
- data/lib/roda/plugins/multi_route.rb +14 -3
- data/lib/roda/plugins/not_allowed.rb +10 -3
- data/lib/roda/plugins/path.rb +38 -0
- data/lib/roda/plugins/symbol_matchers.rb +1 -1
- data/lib/roda/plugins/view_subdirs.rb +7 -1
- data/lib/roda/version.rb +1 -1
- data/spec/integration_spec.rb +95 -3
- data/spec/plugin/_erubis_escaping_spec.rb +1 -0
- data/spec/plugin/assets_spec.rb +86 -0
- data/spec/plugin/error_email_spec.rb +68 -0
- data/spec/plugin/multi_route_spec.rb +22 -0
- data/spec/plugin/not_allowed_spec.rb +13 -0
- data/spec/plugin/path_spec.rb +29 -0
- metadata +104 -66
- checksums.yaml +0 -7
@@ -0,0 +1,277 @@
|
|
1
|
+
require "tilt"
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
module Assets
|
7
|
+
def self.load_dependencies(app, opts={})
|
8
|
+
app.plugin :render
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.configure(app, opts={}, &block)
|
12
|
+
if app.opts[:assets]
|
13
|
+
app.opts[:assets].merge!(opts)
|
14
|
+
else
|
15
|
+
app.opts[:assets] = opts.dup
|
16
|
+
end
|
17
|
+
|
18
|
+
opts = app.opts[:assets]
|
19
|
+
opts[:css] ||= []
|
20
|
+
opts[:js] ||= []
|
21
|
+
opts[:js_folder] ||= 'js'
|
22
|
+
opts[:css_folder] ||= 'css'
|
23
|
+
opts[:path] ||= File.expand_path("assets", Dir.pwd)
|
24
|
+
opts[:compiled_path] ||= opts[:path]
|
25
|
+
opts[:compiled_name] ||= 'compiled.roda.assets'
|
26
|
+
opts[:concat_name] ||= 'concat.roda.assets'
|
27
|
+
opts[:route] ||= 'assets'
|
28
|
+
opts[:css_engine] ||= 'scss'
|
29
|
+
opts[:js_engine] ||= 'coffee'
|
30
|
+
opts[:concat] ||= false
|
31
|
+
opts[:compiled] ||= false
|
32
|
+
opts[:headers] ||= {}
|
33
|
+
|
34
|
+
if opts.fetch(:cache, true)
|
35
|
+
opts[:cache] = app.thread_safe_cache
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
# Copy the assets options into the subclass, duping
|
41
|
+
# them as necessary to prevent changes in the subclass
|
42
|
+
# affecting the parent class.
|
43
|
+
def inherited(subclass)
|
44
|
+
super
|
45
|
+
opts = subclass.opts[:assets] = assets_opts.dup
|
46
|
+
opts[:cache] = thread_safe_cache if opts[:cache]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return the assets options for this class.
|
50
|
+
def assets_opts
|
51
|
+
opts[:assets]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Generates a unique id, this is used to keep concat/compiled files
|
55
|
+
# from caching in the browser when they are generated
|
56
|
+
def assets_unique_id type
|
57
|
+
if unique_id = instance_variable_get("@#{type}")
|
58
|
+
unique_id
|
59
|
+
else
|
60
|
+
path = "#{assets_opts[:compiled_path]}/#{assets_opts[:"#{type}_folder"]}"
|
61
|
+
file = "#{path}/#{assets_opts[:compiled_name]}.#{type}"
|
62
|
+
content = File.exist?(file) ? File.read(file) : ''
|
63
|
+
|
64
|
+
instance_variable_set("@#{type}", Digest::SHA1.hexdigest(content))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def compile_assets(concat_only = false)
|
69
|
+
assets_opts[:concat_only] = concat_only
|
70
|
+
|
71
|
+
%w(css js).each do |type|
|
72
|
+
files = assets_opts[type.to_sym]
|
73
|
+
|
74
|
+
if files.is_a? Array
|
75
|
+
compile_process_files files, type, type
|
76
|
+
else
|
77
|
+
files.each do |folder, f|
|
78
|
+
compile_process_files f, type, folder
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def compile_process_files files, type, folder
|
87
|
+
require 'yuicompressor'
|
88
|
+
|
89
|
+
r = new
|
90
|
+
|
91
|
+
content = ''
|
92
|
+
|
93
|
+
files.each do |file|
|
94
|
+
if type != folder
|
95
|
+
file = "#{folder}/#{file}"
|
96
|
+
end
|
97
|
+
|
98
|
+
content += r.read_asset_file file, type
|
99
|
+
end
|
100
|
+
|
101
|
+
path = assets_opts[:compiled_path] \
|
102
|
+
+ "/#{assets_opts[:"#{type}_folder"]}/" \
|
103
|
+
+ assets_opts[:compiled_name] \
|
104
|
+
+ (type != folder ? ".#{folder}" : '') \
|
105
|
+
+ ".#{type}"
|
106
|
+
|
107
|
+
unless assets_opts[:concat_only]
|
108
|
+
content = YUICompressor.send("compress_#{type}", content, munge: true)
|
109
|
+
end
|
110
|
+
|
111
|
+
File.write path, content
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
module InstanceMethods
|
116
|
+
# This will ouput the files with the appropriate tags
|
117
|
+
def assets folder, options = {}
|
118
|
+
attrs = options.map{|k,v| "#{k}=\"#{v}\""}
|
119
|
+
tags = []
|
120
|
+
folder = [folder] unless folder.is_a? Array
|
121
|
+
type = folder.first
|
122
|
+
attr = type.to_s == 'js' ? 'src' : 'href'
|
123
|
+
path = "#{assets_opts[:route]}/#{assets_opts[:"#{type}_folder"]}"
|
124
|
+
files = folder.length == 1 \
|
125
|
+
? assets_opts[:"#{folder[0]}"] \
|
126
|
+
: assets_opts[:"#{folder[0]}"][:"#{folder[1]}"]
|
127
|
+
|
128
|
+
# Create a tag for each individual file
|
129
|
+
if !assets_opts[:concat]
|
130
|
+
files.each do |file|
|
131
|
+
# This allows you to do things like:
|
132
|
+
# assets_opts[:css] = ['app', './bower/jquery/jquery-min.js']
|
133
|
+
file.gsub!(/\./, '$2E')
|
134
|
+
# Add tags to the tags array
|
135
|
+
tags << send(
|
136
|
+
"#{type}_assets_tag",
|
137
|
+
attrs.dup.unshift( "#{attr}=\"/#{path}/#{file}.#{type}\"").join(' ')
|
138
|
+
)
|
139
|
+
end
|
140
|
+
# Return tags as string
|
141
|
+
tags.join "\n"
|
142
|
+
else
|
143
|
+
name = assets_opts[:compiled] ? assets_opts[:compiled_name] : assets_opts[:concat_name]
|
144
|
+
name = "#{name}/#{folder.join('-')}"
|
145
|
+
# Generate unique url so middleware knows to check for # compile/concat
|
146
|
+
attrs.unshift("#{attr}=\"/#{path}/#{name}/#{assets_unique_id(type)}.#{type}\"")
|
147
|
+
# Return tag string
|
148
|
+
send("#{type}_assets_tag", attrs.join(' '))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def render_asset(file, type)
|
153
|
+
file.gsub!(/(\$2E|%242E)/, '.')
|
154
|
+
|
155
|
+
if !assets_opts[:compiled] && !assets_opts[:concat]
|
156
|
+
read_asset_file file, type
|
157
|
+
elsif assets_opts[:compiled]
|
158
|
+
folder = file.split('/')[1].split('-', 2)
|
159
|
+
path = assets_opts[:compiled_path] \
|
160
|
+
+ "/#{assets_opts[:"#{type}_folder"]}/" \
|
161
|
+
+ assets_opts[:compiled_name] \
|
162
|
+
+ (folder.length > 1 ? ".#{folder[1]}" : '') \
|
163
|
+
+ ".#{type}"
|
164
|
+
|
165
|
+
File.read path
|
166
|
+
elsif assets_opts[:concat]
|
167
|
+
# "concat.roda.assets/css/123"
|
168
|
+
content = ''
|
169
|
+
folder = file.split('/')[1].split('-', 2)
|
170
|
+
files = folder.length == 1 \
|
171
|
+
? assets_opts[:"#{folder[0]}"] \
|
172
|
+
: assets_opts[:"#{folder[0]}"][:"#{folder[1]}"]
|
173
|
+
|
174
|
+
files.each { |f| content += read_asset_file f, type }
|
175
|
+
|
176
|
+
content
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def read_asset_file file, type
|
181
|
+
folder = assets_opts[:"#{type}_folder"]
|
182
|
+
|
183
|
+
# If there is no file it must be a remote file request.
|
184
|
+
# Lets set the file to the url
|
185
|
+
if file == ''
|
186
|
+
route = assets_opts[:route]
|
187
|
+
file = env['SCRIPT_NAME'].gsub(/^\/#{route}\/#{folder}\//, '')
|
188
|
+
end
|
189
|
+
|
190
|
+
if !file[/^\.\//] && !file[/^http/]
|
191
|
+
path = assets_opts[:path] + '/' + folder + "/#{file}"
|
192
|
+
else
|
193
|
+
path = file
|
194
|
+
end
|
195
|
+
|
196
|
+
engine = assets_opts[:"#{type}_engine"]
|
197
|
+
|
198
|
+
# render via tilt
|
199
|
+
if File.exists? "#{path}.#{engine}"
|
200
|
+
render path: "#{path}.#{engine}"
|
201
|
+
# read file directly
|
202
|
+
elsif File.exists? "#{path}.#{type}"
|
203
|
+
File.read "#{path}.#{type}"
|
204
|
+
# grab remote file content
|
205
|
+
elsif file[/^http/]
|
206
|
+
open(file).read
|
207
|
+
# as a last attempt lets see if the file can be rendered by tilt
|
208
|
+
# otherwise load the file directly
|
209
|
+
elsif File.exists? path
|
210
|
+
begin
|
211
|
+
render path: path
|
212
|
+
rescue
|
213
|
+
File.read path
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Shortcut for class opts
|
219
|
+
def assets_opts
|
220
|
+
self.class.assets_opts
|
221
|
+
end
|
222
|
+
|
223
|
+
# Shortcut for class assets unique id
|
224
|
+
def assets_unique_id(*args)
|
225
|
+
self.class.assets_unique_id(*args)
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
# CSS tag template
|
231
|
+
# <link rel="stylesheet" href="theme.css">
|
232
|
+
def css_assets_tag attrs
|
233
|
+
"<link rel=\"stylesheet\" #{attrs} />"
|
234
|
+
end
|
235
|
+
|
236
|
+
# JS tag template
|
237
|
+
# <script src="scriptfile.js"></script>
|
238
|
+
def js_assets_tag attrs
|
239
|
+
"<script type=\"text/javascript\" #{attrs}></script>"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
module RequestClassMethods
|
244
|
+
# Shortcut for roda class asset opts
|
245
|
+
def assets_opts
|
246
|
+
roda_class.assets_opts
|
247
|
+
end
|
248
|
+
|
249
|
+
# The regex for the assets route
|
250
|
+
def assets_route_regex
|
251
|
+
Regexp.new(
|
252
|
+
assets_opts[:route] + '/' +
|
253
|
+
"(#{assets_opts[:"css_folder"]}|#{assets_opts[:"js_folder"]})" +
|
254
|
+
'/(.*)(?:\.(css|js)|http.*)$'
|
255
|
+
)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
module RequestMethods
|
260
|
+
# Handles calls to the assets route
|
261
|
+
def assets
|
262
|
+
on self.class.assets_route_regex do |type, file|
|
263
|
+
content_type = type == 'css' ? 'text/css' : 'application/javascript'
|
264
|
+
|
265
|
+
response.headers.merge!({
|
266
|
+
"Content-Type" => content_type + '; charset=UTF-8',
|
267
|
+
}.merge(scope.assets_opts[:headers]))
|
268
|
+
|
269
|
+
scope.render_asset file, type
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
register_plugin(:assets, Assets)
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,110 @@
|
|
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
|
+
:emailer=>lambda{|h| Net::SMTP.start(h[:host]){|s| s.send_message(h[:message], h[:from], h[:to])}},
|
35
|
+
:default_headers=>lambda do |h, e|
|
36
|
+
{'From'=>h[:from], 'To'=>h[:to], 'Subject'=>"#{h[:prefix]}#{e.class}: #{e.message}"}
|
37
|
+
end,
|
38
|
+
:body=>lambda do |s, e|
|
39
|
+
format = lambda{|h| h.map{|k, v| "#{k.inspect} => #{v.inspect}"}.sort.join("\n")}
|
40
|
+
|
41
|
+
message = <<END
|
42
|
+
Path: #{s.request.full_path_info}
|
43
|
+
|
44
|
+
Backtrace:
|
45
|
+
|
46
|
+
#{e.backtrace.join("\n")}
|
47
|
+
|
48
|
+
ENV:
|
49
|
+
|
50
|
+
#{format[s.env]}
|
51
|
+
END
|
52
|
+
unless s.request.params.empty?
|
53
|
+
message << <<END
|
54
|
+
|
55
|
+
Params:
|
56
|
+
|
57
|
+
#{format[s.request.params]}
|
58
|
+
END
|
59
|
+
end
|
60
|
+
|
61
|
+
if s.env['rack.session']
|
62
|
+
message << <<END
|
63
|
+
|
64
|
+
Session:
|
65
|
+
|
66
|
+
#{format[s.session]}
|
67
|
+
END
|
68
|
+
end
|
69
|
+
|
70
|
+
message
|
71
|
+
end
|
72
|
+
}
|
73
|
+
|
74
|
+
# Set default opts for plugin. See ErrorEmail module RDoc for options.
|
75
|
+
def self.configure(app, opts={})
|
76
|
+
email_opts = app.opts[:error_email] ||= DEFAULTS
|
77
|
+
email_opts = email_opts.merge(opts)
|
78
|
+
unless email_opts[:to] && email_opts[:from]
|
79
|
+
raise RodaError, "must provide :to and :from options to error_email plugin"
|
80
|
+
end
|
81
|
+
app.opts[:error_email] = email_opts
|
82
|
+
end
|
83
|
+
|
84
|
+
module ClassMethods
|
85
|
+
# Dup the error email opts in the subclass so changes to the subclass do not affect
|
86
|
+
# the superclass.
|
87
|
+
def inherited(subclass)
|
88
|
+
super
|
89
|
+
subclass.opts[:error_email] = subclass.opts[:error_email].dup
|
90
|
+
subclass.opts[:error_email][:headers] = subclass.opts[:error_email][:headers].dup
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
module InstanceMethods
|
95
|
+
# Send an email for the given error.
|
96
|
+
def error_email(e)
|
97
|
+
email_opts = self.class.opts[:error_email].dup
|
98
|
+
headers = email_opts[:default_headers].call(email_opts, e)
|
99
|
+
headers = headers.merge(email_opts[:headers])
|
100
|
+
headers = headers.map{|k,v| "#{k}: #{v}"}.sort.join("\n")
|
101
|
+
body = email_opts[:body].call(self, e)
|
102
|
+
email_opts[:message] = "#{headers}\n\n#{body}"
|
103
|
+
email_opts[:emailer].call(email_opts)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
register_plugin(:error_email, ErrorEmail)
|
109
|
+
end
|
110
|
+
end
|
@@ -64,7 +64,7 @@ class Roda
|
|
64
64
|
subclass.instance_variable_set(:@named_routes, @named_routes.dup)
|
65
65
|
end
|
66
66
|
|
67
|
-
#
|
67
|
+
# The names for the currently stored named routes
|
68
68
|
def named_routes
|
69
69
|
@named_routes.keys
|
70
70
|
end
|
@@ -74,12 +74,13 @@ class Roda
|
|
74
74
|
@named_routes[name]
|
75
75
|
end
|
76
76
|
|
77
|
-
# If the given route has a
|
77
|
+
# If the given route has a name, treat it as a named route and
|
78
78
|
# store the route block. Otherwise, this is the main route, so
|
79
79
|
# call super.
|
80
80
|
def route(name=nil, &block)
|
81
81
|
if name
|
82
82
|
@named_routes[name] = block
|
83
|
+
self::RodaRequest.clear_named_route_regexp!
|
83
84
|
else
|
84
85
|
super(&block)
|
85
86
|
end
|
@@ -87,9 +88,19 @@ class Roda
|
|
87
88
|
end
|
88
89
|
|
89
90
|
module RequestClassMethods
|
91
|
+
# Clear cached regexp for named routes, it will be regenerated
|
92
|
+
# the next time it is needed.
|
93
|
+
#
|
94
|
+
# This shouldn't be an issue in production applications, but
|
95
|
+
# during development it's useful to support new named routes
|
96
|
+
# being added while the application is running.
|
97
|
+
def clear_named_route_regexp!
|
98
|
+
@named_route_regexp = nil
|
99
|
+
end
|
100
|
+
|
90
101
|
# A regexp matching any of the current named routes.
|
91
102
|
def named_route_regexp
|
92
|
-
@named_route_regexp ||= /(#{Regexp.union(roda_class.named_routes)})/
|
103
|
+
@named_route_regexp ||= /(#{Regexp.union(roda_class.named_routes.select{|s| s.is_a?(String)})})/
|
93
104
|
end
|
94
105
|
end
|
95
106
|
|