pirj-sinatra-contrib 1.3.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/LICENSE +20 -0
- data/README.md +135 -0
- data/Rakefile +61 -0
- data/ideas.md +29 -0
- data/lib/sinatra/capture.rb +42 -0
- data/lib/sinatra/config_file.rb +151 -0
- data/lib/sinatra/content_for.rb +111 -0
- data/lib/sinatra/contrib.rb +39 -0
- data/lib/sinatra/contrib/all.rb +2 -0
- data/lib/sinatra/contrib/setup.rb +53 -0
- data/lib/sinatra/contrib/version.rb +45 -0
- data/lib/sinatra/decompile.rb +113 -0
- data/lib/sinatra/engine_tracking.rb +96 -0
- data/lib/sinatra/extension.rb +95 -0
- data/lib/sinatra/json.rb +134 -0
- data/lib/sinatra/link_header.rb +132 -0
- data/lib/sinatra/namespace.rb +282 -0
- data/lib/sinatra/reloader.rb +384 -0
- data/lib/sinatra/respond_with.rb +245 -0
- data/lib/sinatra/streaming.rb +267 -0
- data/lib/sinatra/test_helpers.rb +87 -0
- data/sinatra-contrib.gemspec +121 -0
- data/spec/capture_spec.rb +80 -0
- data/spec/config_file/key_value.yml +6 -0
- data/spec/config_file/missing_env.yml +4 -0
- data/spec/config_file/with_envs.yml +7 -0
- data/spec/config_file/with_nested_envs.yml +11 -0
- data/spec/config_file_spec.rb +44 -0
- data/spec/content_for/different_key.erb +1 -0
- data/spec/content_for/different_key.erubis +1 -0
- data/spec/content_for/different_key.haml +2 -0
- data/spec/content_for/different_key.slim +2 -0
- data/spec/content_for/layout.erb +1 -0
- data/spec/content_for/layout.erubis +1 -0
- data/spec/content_for/layout.haml +1 -0
- data/spec/content_for/layout.slim +1 -0
- data/spec/content_for/multiple_blocks.erb +4 -0
- data/spec/content_for/multiple_blocks.erubis +4 -0
- data/spec/content_for/multiple_blocks.haml +8 -0
- data/spec/content_for/multiple_blocks.slim +8 -0
- data/spec/content_for/multiple_yields.erb +3 -0
- data/spec/content_for/multiple_yields.erubis +3 -0
- data/spec/content_for/multiple_yields.haml +3 -0
- data/spec/content_for/multiple_yields.slim +3 -0
- data/spec/content_for/passes_values.erb +1 -0
- data/spec/content_for/passes_values.erubis +1 -0
- data/spec/content_for/passes_values.haml +1 -0
- data/spec/content_for/passes_values.slim +1 -0
- data/spec/content_for/same_key.erb +1 -0
- data/spec/content_for/same_key.erubis +1 -0
- data/spec/content_for/same_key.haml +2 -0
- data/spec/content_for/same_key.slim +2 -0
- data/spec/content_for/takes_values.erb +1 -0
- data/spec/content_for/takes_values.erubis +1 -0
- data/spec/content_for/takes_values.haml +3 -0
- data/spec/content_for/takes_values.slim +3 -0
- data/spec/content_for_spec.rb +201 -0
- data/spec/decompile_spec.rb +44 -0
- data/spec/extension_spec.rb +33 -0
- data/spec/json_spec.rb +115 -0
- data/spec/link_header_spec.rb +100 -0
- data/spec/namespace/foo.erb +1 -0
- data/spec/namespace/nested/foo.erb +1 -0
- data/spec/namespace_spec.rb +623 -0
- data/spec/okjson.rb +581 -0
- data/spec/reloader/app.rb.erb +40 -0
- data/spec/reloader_spec.rb +441 -0
- data/spec/respond_with/bar.erb +1 -0
- data/spec/respond_with/bar.json.erb +1 -0
- data/spec/respond_with/foo.html.erb +1 -0
- data/spec/respond_with/not_html.sass +2 -0
- data/spec/respond_with_spec.rb +289 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/streaming_spec.rb +436 -0
- metadata +252 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
|
5
|
+
# = Sinatra::Reloader
|
6
|
+
#
|
7
|
+
# Extension to reload modified files. Useful during development,
|
8
|
+
# since it will automatically require files defining routes, filters,
|
9
|
+
# error handlers and inline templates, with every incoming request,
|
10
|
+
# but only if they have been updated.
|
11
|
+
#
|
12
|
+
# == Usage
|
13
|
+
#
|
14
|
+
# === Classic Application
|
15
|
+
#
|
16
|
+
# To enable the realoader in a classic application all you need to do is
|
17
|
+
# require it:
|
18
|
+
#
|
19
|
+
# require "sinatra"
|
20
|
+
# require "sinatra/reloader" if development?
|
21
|
+
#
|
22
|
+
# # Your classic application code goes here...
|
23
|
+
#
|
24
|
+
# === Modular Application
|
25
|
+
#
|
26
|
+
# To enable the realoader in a modular application all you need to do is
|
27
|
+
# require it, and then, register it:
|
28
|
+
#
|
29
|
+
# require "sinatra/base"
|
30
|
+
# require "sinatra/reloader"
|
31
|
+
#
|
32
|
+
# class MyApp < Sinatra::Base
|
33
|
+
# configure :development do
|
34
|
+
# register Sinatra::Reloader
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# # Your modular application code goes here...
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# == Changing the Reloading Policy
|
41
|
+
#
|
42
|
+
# You can refine the reloading policy with +also_reload+ and
|
43
|
+
# +dont_reload+, to customize which files should, and should not, be
|
44
|
+
# reloaded, respectively.
|
45
|
+
#
|
46
|
+
# === Classic Application
|
47
|
+
#
|
48
|
+
# Simply call the methods:
|
49
|
+
#
|
50
|
+
# require "sinatra"
|
51
|
+
# require "sinatra/reloader" if development?
|
52
|
+
#
|
53
|
+
# also_reload '/path/to/some/file'
|
54
|
+
# dont_reload '/path/to/other/file'
|
55
|
+
#
|
56
|
+
# # Your classic application code goes here...
|
57
|
+
#
|
58
|
+
# === Modular Application
|
59
|
+
#
|
60
|
+
# Call the methods inside the +configure+ block:
|
61
|
+
#
|
62
|
+
# require "sinatra/base"
|
63
|
+
# require "sinatra/reloader"
|
64
|
+
#
|
65
|
+
# class MyApp < Sinatra::Base
|
66
|
+
# configure :development do
|
67
|
+
# register Sinatra::Reloader
|
68
|
+
# also_reload '/path/to/some/file'
|
69
|
+
# dont_reload '/path/to/other/file'
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# # Your modular application code goes here...
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
module Reloader
|
76
|
+
|
77
|
+
# Watches a file so it can tell when it has been updated, and what
|
78
|
+
# elements contains.
|
79
|
+
class Watcher
|
80
|
+
|
81
|
+
# Represents an element of a Sinatra application that may need to
|
82
|
+
# be reloaded. An element could be:
|
83
|
+
# * a route
|
84
|
+
# * a filter
|
85
|
+
# * an error handler
|
86
|
+
# * a middleware
|
87
|
+
# * inline templates
|
88
|
+
#
|
89
|
+
# Its +representation+ attribute is there to allow to identify the
|
90
|
+
# element within an application, that is, to match it with its
|
91
|
+
# Sinatra's internal representation.
|
92
|
+
class Element < Struct.new(:type, :representation)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Collection of file +Watcher+ that can be associated with a
|
96
|
+
# Sinatra application. That way, we can know which files belong
|
97
|
+
# to a given application and which files have been modified. It
|
98
|
+
# also provides a mechanism to inform a Watcher the elements
|
99
|
+
# defined in the file being watched and if it changes should be
|
100
|
+
# ignored.
|
101
|
+
class List
|
102
|
+
@app_list_map = Hash.new { |hash, key| hash[key] = new }
|
103
|
+
|
104
|
+
# Returns the +List+ for the application +app+.
|
105
|
+
def self.for(app)
|
106
|
+
@app_list_map[app]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Creates a new +List+ instance.
|
110
|
+
def initialize
|
111
|
+
@path_watcher_map = Hash.new do |hash, key|
|
112
|
+
hash[key] = Watcher.new(key)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Lets the +Watcher+ for the file localted at +path+ know that the
|
117
|
+
# +element+ is defined there, and adds the +Watcher+ to the +List+,
|
118
|
+
# if it isn't already there.
|
119
|
+
def watch(path, element)
|
120
|
+
watcher_for(path).elements << element
|
121
|
+
end
|
122
|
+
|
123
|
+
# Tells the +Watcher+ for the file located at +path+ to ignore
|
124
|
+
# the file changes, and adds the +Watcher+ to the +List+, if
|
125
|
+
# it isn't already there.
|
126
|
+
def ignore(path)
|
127
|
+
watcher_for(path).ignore
|
128
|
+
end
|
129
|
+
|
130
|
+
# Adds a +Watcher+ for the file located at +path+ to the
|
131
|
+
# +List+, if it isn't already there.
|
132
|
+
def watcher_for(path)
|
133
|
+
@path_watcher_map[File.expand_path(path)]
|
134
|
+
end
|
135
|
+
alias watch_file watcher_for
|
136
|
+
|
137
|
+
# Returns an array with all the watchers in the +List+.
|
138
|
+
def watchers
|
139
|
+
@path_watcher_map.values
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns an array with all the watchers in the +List+ that
|
143
|
+
# have been updated.
|
144
|
+
def updated
|
145
|
+
watchers.find_all(&:updated?)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
attr_reader :path, :elements, :mtime
|
150
|
+
|
151
|
+
# Creates a new +Watcher+ instance for the file located at +path+.
|
152
|
+
def initialize(path)
|
153
|
+
@path, @elements = path, []
|
154
|
+
update
|
155
|
+
end
|
156
|
+
|
157
|
+
# Indicates whether or not the file being watched has been modified.
|
158
|
+
def updated?
|
159
|
+
!ignore? && !removed? && mtime != File.mtime(path)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Updates the file being watched mtime.
|
163
|
+
def update
|
164
|
+
@mtime = File.mtime(path)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Indicates whether or not the file being watched has inline
|
168
|
+
# templates.
|
169
|
+
def inline_templates?
|
170
|
+
elements.any? { |element| element.type == :inline_templates }
|
171
|
+
end
|
172
|
+
|
173
|
+
# Informs that the modifications to the file being watched
|
174
|
+
# should be ignored.
|
175
|
+
def ignore
|
176
|
+
@ignore = true
|
177
|
+
end
|
178
|
+
|
179
|
+
# Indicates whether or not the modifications to the file being
|
180
|
+
# watched should be ignored.
|
181
|
+
def ignore?
|
182
|
+
!!@ignore
|
183
|
+
end
|
184
|
+
|
185
|
+
# Indicates whether or not the file being watched has been removed.
|
186
|
+
def removed?
|
187
|
+
!File.exist?(path)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# When the extension is registed it extends the Sinatra application
|
192
|
+
# +klass+ with the modules +BaseMethods+ and +ExtensionMethods+ and
|
193
|
+
# defines a before filter to +perform+ the reload of the modified files.
|
194
|
+
def self.registered(klass)
|
195
|
+
@reloader_loaded_in ||= {}
|
196
|
+
return if @reloader_loaded_in[klass]
|
197
|
+
|
198
|
+
@reloader_loaded_in[klass] = true
|
199
|
+
|
200
|
+
klass.extend BaseMethods
|
201
|
+
klass.extend ExtensionMethods
|
202
|
+
klass.set(:reloader) { klass.development? }
|
203
|
+
klass.set(:reload_templates) { klass.reloader? }
|
204
|
+
klass.before do
|
205
|
+
if klass.reloader?
|
206
|
+
if Reloader.thread_safe?
|
207
|
+
Thread.exclusive { Reloader.perform(klass) }
|
208
|
+
else
|
209
|
+
Reloader.perform(klass)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Reloads the modified files, adding, updating and removing the
|
216
|
+
# needed elements.
|
217
|
+
def self.perform(klass)
|
218
|
+
Watcher::List.for(klass).updated.each do |watcher|
|
219
|
+
klass.set(:inline_templates, watcher.path) if watcher.inline_templates?
|
220
|
+
watcher.elements.each { |element| klass.deactivate(element) }
|
221
|
+
$LOADED_FEATURES.delete(watcher.path)
|
222
|
+
require watcher.path
|
223
|
+
watcher.update
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Indicates whether or not we can and need to run thread-safely.
|
228
|
+
def self.thread_safe?
|
229
|
+
Thread and Thread.list.size > 1 and Thread.respond_to?(:exclusive)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Contains the methods defined in Sinatra::Base that are overriden.
|
233
|
+
module BaseMethods
|
234
|
+
# Does everything Sinatra::Base#route does, but it also tells the
|
235
|
+
# +Watcher::List+ for the Sinatra application to watch the defined
|
236
|
+
# route.
|
237
|
+
#
|
238
|
+
# Note: We are using #compile! so we don't interfere with extensions
|
239
|
+
# changing #route.
|
240
|
+
def compile!(verb, path, block, options = {})
|
241
|
+
source_location = block.respond_to?(:source_location) ?
|
242
|
+
block.source_location.first : caller_files[1]
|
243
|
+
signature = super
|
244
|
+
watch_element(
|
245
|
+
source_location, :route, { :verb => verb, :signature => signature }
|
246
|
+
)
|
247
|
+
signature
|
248
|
+
end
|
249
|
+
|
250
|
+
# Does everything Sinatra::Base#inline_templates= does, but it also
|
251
|
+
# tells the +Watcher::List+ for the Sinatra application to watch the
|
252
|
+
# inline templates in +file+ or the file who made the call to this
|
253
|
+
# method.
|
254
|
+
def inline_templates=(file=nil)
|
255
|
+
file = (file.nil? || file == true) ?
|
256
|
+
(caller_files[1] || File.expand_path($0)) : file
|
257
|
+
watch_element(file, :inline_templates)
|
258
|
+
super
|
259
|
+
end
|
260
|
+
|
261
|
+
# Does everything Sinatra::Base#use does, but it also tells the
|
262
|
+
# +Watcher::List+ for the Sinatra application to watch the middleware
|
263
|
+
# being used.
|
264
|
+
def use(middleware, *args, &block)
|
265
|
+
path = caller_files[1] || File.expand_path($0)
|
266
|
+
watch_element(path, :middleware, [middleware, args, block])
|
267
|
+
super
|
268
|
+
end
|
269
|
+
|
270
|
+
# Does everything Sinatra::Base#add_filter does, but it also tells
|
271
|
+
# the +Watcher::List+ for the Sinatra application to watch the defined
|
272
|
+
# filter.
|
273
|
+
def add_filter(type, path = nil, options = {}, &block)
|
274
|
+
source_location = block.respond_to?(:source_location) ?
|
275
|
+
block.source_location.first : caller_files[1]
|
276
|
+
result = super
|
277
|
+
watch_element(source_location, :"#{type}_filter", filters[type].last)
|
278
|
+
result
|
279
|
+
end
|
280
|
+
|
281
|
+
# Does everything Sinatra::Base#error does, but it also tells the
|
282
|
+
# +Watcher::List+ for the Sinatra application to watch the defined
|
283
|
+
# error handler.
|
284
|
+
def error(*codes, &block)
|
285
|
+
path = caller_files[1] || File.expand_path($0)
|
286
|
+
result = super
|
287
|
+
codes.each do |c|
|
288
|
+
watch_element(path, :error, :code => c, :handler => @errors[c])
|
289
|
+
end
|
290
|
+
result
|
291
|
+
end
|
292
|
+
|
293
|
+
# Does everything Sinatra::Base#register does, but it also lets the
|
294
|
+
# reloader know that an extension is being registered, because the
|
295
|
+
# elements defined in its +registered+ method need a special treatment.
|
296
|
+
def register(*extensions, &block)
|
297
|
+
start_registering_extension
|
298
|
+
result = super
|
299
|
+
stop_registering_extension
|
300
|
+
result
|
301
|
+
end
|
302
|
+
|
303
|
+
# Does everything Sinatra::Base#register does and then registers the
|
304
|
+
# reloader in the +subclass+.
|
305
|
+
def inherited(subclass)
|
306
|
+
result = super
|
307
|
+
subclass.register Sinatra::Reloader
|
308
|
+
result
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Contains the methods that the extension adds to the Sinatra application.
|
313
|
+
module ExtensionMethods
|
314
|
+
# Removes the +element+ from the Sinatra application.
|
315
|
+
def deactivate(element)
|
316
|
+
case element.type
|
317
|
+
when :route then
|
318
|
+
verb = element.representation[:verb]
|
319
|
+
signature = element.representation[:signature]
|
320
|
+
(routes[verb] ||= []).delete(signature)
|
321
|
+
when :middleware then
|
322
|
+
@middleware.delete(element.representation)
|
323
|
+
when :before_filter then
|
324
|
+
filters[:before].delete(element.representation)
|
325
|
+
when :after_filter then
|
326
|
+
filters[:after].delete(element.representation)
|
327
|
+
when :error then
|
328
|
+
code = element.representation[:code]
|
329
|
+
handler = element.representation[:handler]
|
330
|
+
@errors.delete(code) if @errors[code] == handler
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Indicates with a +glob+ which files should be reloaded if they
|
335
|
+
# have been modified. It can be called several times.
|
336
|
+
def also_reload(glob)
|
337
|
+
Dir[glob].each { |path| Watcher::List.for(self).watch_file(path) }
|
338
|
+
end
|
339
|
+
|
340
|
+
# Indicates with a +glob+ which files should not be reloaded even if
|
341
|
+
# they have been modified. It can be called several times.
|
342
|
+
def dont_reload(glob)
|
343
|
+
Dir[glob].each { |path| Watcher::List.for(self).ignore(path) }
|
344
|
+
end
|
345
|
+
|
346
|
+
private
|
347
|
+
|
348
|
+
attr_reader :register_path
|
349
|
+
|
350
|
+
# Indicates an extesion is being registered.
|
351
|
+
def start_registering_extension
|
352
|
+
@register_path = caller_files[2]
|
353
|
+
end
|
354
|
+
|
355
|
+
# Indicates the extesion has already been registered.
|
356
|
+
def stop_registering_extension
|
357
|
+
@register_path = nil
|
358
|
+
end
|
359
|
+
|
360
|
+
# Indicates whether or not an extension is being registered.
|
361
|
+
def registering_extension?
|
362
|
+
!register_path.nil?
|
363
|
+
end
|
364
|
+
|
365
|
+
# Builds a Watcher::Element from +type+ and +representation+ and
|
366
|
+
# tells the Watcher::List for the current application to watch it
|
367
|
+
# in the file located at +path+.
|
368
|
+
#
|
369
|
+
# If an extension is being registered, it also tells the list to
|
370
|
+
# watch it in the file where the extesion has been registered.
|
371
|
+
# This prevents the duplication of the elements added by the
|
372
|
+
# extension in its +registered+ method with every reload.
|
373
|
+
def watch_element(path, type, representation=nil)
|
374
|
+
list = Watcher::List.for(self)
|
375
|
+
element = Watcher::Element.new(type, representation)
|
376
|
+
list.watch(path, element)
|
377
|
+
list.watch(register_path, element) if registering_extension?
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
register Reloader
|
383
|
+
Delegator.delegate :also_reload, :dont_reload
|
384
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/json'
|
3
|
+
|
4
|
+
module Sinatra
|
5
|
+
##
|
6
|
+
# = Sinatra::RespondWith
|
7
|
+
#
|
8
|
+
# This extensions lets Sinatra automatically choose what template to render or
|
9
|
+
# action to perform depending on the request's Accept header.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# # Without Sinatra::RespondWith
|
14
|
+
# get '/' do
|
15
|
+
# data = { :name => 'example' }
|
16
|
+
# request.accept.each do |type|
|
17
|
+
# case type
|
18
|
+
# when 'text/html'
|
19
|
+
# halt haml(:index, :locals => data)
|
20
|
+
# when 'text/json'
|
21
|
+
# halt data.to_json
|
22
|
+
# when 'application/atom+xml'
|
23
|
+
# halt nokogiri(:'index.atom', :locals => data)
|
24
|
+
# when 'application/xml', 'text/xml'
|
25
|
+
# halt nokogiri(:'index.xml', :locals => data)
|
26
|
+
# when 'text/plain'
|
27
|
+
# halt 'just an example'
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
# error 406
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # With Sinatra::RespondWith
|
34
|
+
# get '/' do
|
35
|
+
# respond_with :index, :name => 'example' do |f|
|
36
|
+
# f.txt { 'just an example' }
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# Both helper methods +respond_to+ and +respond_with+ let you define custom
|
41
|
+
# handlers like the one above for +text/plain+. +respond_with+ additionally
|
42
|
+
# takes a template name and/or an object to offer the following default
|
43
|
+
# behavior:
|
44
|
+
#
|
45
|
+
# * If a template name is given, search for a template called
|
46
|
+
# +name.format.engine+ (+index.xml.nokogiri+ in the above example).
|
47
|
+
# * If a template name is given, search for a templated called +name.engine+
|
48
|
+
# for engines known to result in the requested format (+index.haml+).
|
49
|
+
# * If a file extension associated with the mime type is known to Sinatra, and
|
50
|
+
# the object responds to +to_extension+, call that method and use the result
|
51
|
+
# (+data.to_json+).
|
52
|
+
#
|
53
|
+
# == Security
|
54
|
+
#
|
55
|
+
# Since methods are triggered based on client input, this can lead to security
|
56
|
+
# issues (but not as seviere as those might apear in the first place: keep in
|
57
|
+
# mind that only known file extensions are used). You therefore should limit
|
58
|
+
# the possible formats you serve.
|
59
|
+
#
|
60
|
+
# This is possible with the +provides+ condition:
|
61
|
+
#
|
62
|
+
# get '/', :provides => [:html, :json, :xml, :atom] do
|
63
|
+
# respond_with :index, :name => 'example'
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# However, since you have to set +provides+ for every route, this extension
|
67
|
+
# adds a app global (class method) `respond_to`, that let's you define content
|
68
|
+
# types for all routes:
|
69
|
+
#
|
70
|
+
# respond_to :html, :json, :xml, :atom
|
71
|
+
# get('/a') { respond_with :index, :name => 'a' }
|
72
|
+
# get('/b') { respond_with :index, :name => 'b' }
|
73
|
+
#
|
74
|
+
# == Custom Types
|
75
|
+
#
|
76
|
+
# Use the +on+ method for defining actions for custom types:
|
77
|
+
#
|
78
|
+
# get '/' do
|
79
|
+
# respond_to do |f|
|
80
|
+
# f.xml { nokogiri :index }
|
81
|
+
# f.on('application/custom') { custom_action }
|
82
|
+
# f.on('text/*') { data.to_s }
|
83
|
+
# f.on('*/*') { "matches everything" }
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# Definition order does not matter.
|
88
|
+
module RespondWith
|
89
|
+
class Format
|
90
|
+
def initialize(app)
|
91
|
+
@app, @map, @generic, @default = app, {}, {}, nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def on(type, &block)
|
95
|
+
@app.settings.mime_types(type).each do |mime|
|
96
|
+
case mime
|
97
|
+
when '*/*' then @default = block
|
98
|
+
when /^([^\/]+)\/\*$/ then @generic[$1] = block
|
99
|
+
else @map[mime] = block
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def finish
|
105
|
+
yield self if block_given?
|
106
|
+
mime_type = @app.content_type ||
|
107
|
+
@app.request.preferred_type(@map.keys) ||
|
108
|
+
@app.request.preferred_type ||
|
109
|
+
'text/html'
|
110
|
+
type = mime_type.split(/\s*;\s*/, 2).first
|
111
|
+
handlers = [@map[type], @generic[type[/^[^\/]+/]], @default].compact
|
112
|
+
handlers.each do |block|
|
113
|
+
if result = block.call(type)
|
114
|
+
@app.content_type mime_type
|
115
|
+
@app.halt result
|
116
|
+
end
|
117
|
+
end
|
118
|
+
@app.halt 406
|
119
|
+
end
|
120
|
+
|
121
|
+
def method_missing(meth, *args, &block)
|
122
|
+
return super if args.any? or block.nil? or not @app.mime_type(meth)
|
123
|
+
on(meth, &block)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
module Helpers
|
128
|
+
include Sinatra::JSON
|
129
|
+
|
130
|
+
def respond_with(template, object = nil, &block)
|
131
|
+
object, template = template, nil unless Symbol === template
|
132
|
+
format = Format.new(self)
|
133
|
+
format.on "*/*" do |type|
|
134
|
+
exts = settings.ext_map[type]
|
135
|
+
exts << :xml if type.end_with? '+xml'
|
136
|
+
if template
|
137
|
+
args = template_cache.fetch(type, template) { template_for(template, exts) }
|
138
|
+
if args.any?
|
139
|
+
locals = { :object => object }
|
140
|
+
locals.merge! object.to_hash if object.respond_to? :to_hash
|
141
|
+
args << { :locals => locals }
|
142
|
+
halt send(*args)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
if object
|
146
|
+
exts.each do |ext|
|
147
|
+
halt json(object) if ext == :json
|
148
|
+
next unless meth = "to_#{ext}" and object.respond_to? meth
|
149
|
+
halt(*object.send(meth))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
false
|
153
|
+
end
|
154
|
+
format.finish(&block)
|
155
|
+
end
|
156
|
+
|
157
|
+
def respond_to(&block)
|
158
|
+
Format.new(self).finish(&block)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def template_for(name, exts)
|
164
|
+
# in production this is cached, so don't worry to much about runtime
|
165
|
+
possible = []
|
166
|
+
settings.template_engines[:all].each do |engine|
|
167
|
+
exts.each { |ext| possible << [engine, "#{name}.#{ext}"] }
|
168
|
+
end
|
169
|
+
exts.each do |ext|
|
170
|
+
settings.template_engines[ext].each { |e| possible << [e, name] }
|
171
|
+
end
|
172
|
+
possible.each do |engine, template|
|
173
|
+
# not exactly like Tilt[engine], but does not trigger a require
|
174
|
+
klass = Tilt.mappings[Tilt.normalize(engine)].first
|
175
|
+
find_template(settings.views, template, klass) do |file|
|
176
|
+
next unless File.exist? file
|
177
|
+
return settings.rendering_method(engine) << template.to_sym
|
178
|
+
end
|
179
|
+
end
|
180
|
+
[] # nil or false would not be cached
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
attr_accessor :ext_map
|
185
|
+
|
186
|
+
def remap_extensions
|
187
|
+
ext_map.clear
|
188
|
+
Rack::Mime::MIME_TYPES.each { |e,t| ext_map[t] << e[1..-1].to_sym }
|
189
|
+
ext_map['text/javascript'] << 'js'
|
190
|
+
ext_map['text/xml'] << 'xml'
|
191
|
+
end
|
192
|
+
|
193
|
+
def mime_type(*)
|
194
|
+
result = super
|
195
|
+
remap_extensions
|
196
|
+
result
|
197
|
+
end
|
198
|
+
|
199
|
+
def respond_to(*formats)
|
200
|
+
if formats.any?
|
201
|
+
@respond_to ||= []
|
202
|
+
@respond_to.concat formats
|
203
|
+
elsif @respond_to.nil? and superclass.respond_to? :respond_to
|
204
|
+
superclass.respond_to
|
205
|
+
else
|
206
|
+
@respond_to
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def rendering_method(engine)
|
211
|
+
return [engine] if Sinatra::Templates.method_defined? engine
|
212
|
+
return [:mab] if engine.to_sym == :markaby
|
213
|
+
[:render, :engine]
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def compile!(verb, path, block, options = {})
|
219
|
+
options[:provides] ||= respond_to if respond_to
|
220
|
+
super
|
221
|
+
end
|
222
|
+
|
223
|
+
ENGINES = {
|
224
|
+
:css => [:less, :sass, :scss],
|
225
|
+
:xml => [:builder, :nokogiri],
|
226
|
+
:js => [:coffee],
|
227
|
+
:html => [:erb, :erubis, :haml, :slim, :liquid, :radius, :mab, :markdown,
|
228
|
+
:textile, :rdoc],
|
229
|
+
:all => Sinatra::Templates.instance_methods.map(&:to_sym) + [:mab] -
|
230
|
+
[:find_template, :markaby]
|
231
|
+
}
|
232
|
+
|
233
|
+
ENGINES.default = []
|
234
|
+
|
235
|
+
def self.registered(base)
|
236
|
+
base.ext_map = Hash.new { |h,k| h[k] = [] }
|
237
|
+
base.set :template_engines, ENGINES.dup
|
238
|
+
base.remap_extensions
|
239
|
+
base.helpers Helpers
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
register RespondWith
|
244
|
+
Delegator.delegate :respond_to
|
245
|
+
end
|