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.
@@ -16,7 +16,7 @@ class Roda
16
16
  # plugin :all_verbs
17
17
  #
18
18
  # route do |r|
19
- # r.delete
19
+ # r.delete do
20
20
  # # Handle DELETE
21
21
  # end
22
22
  # r.put do
@@ -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
@@ -7,7 +7,7 @@ class Roda
7
7
  # block does not match +/a/b+ by default:
8
8
  #
9
9
  # r.is ['a', 'a/b'] do |path|
10
- # ...
10
+ # # ...
11
11
  # end
12
12
  #
13
13
  # This is because the <tt>'a'</tt> entry in the array matches, which
@@ -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
- # An names for the currently stored named routes
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 named, treat it as a named route and
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