roda-cj 0.9.6 → 1.0.0

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