sinatra-contrib 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +135 -0
  3. data/Rakefile +75 -0
  4. data/ideas.md +29 -0
  5. data/lib/sinatra/capture.rb +42 -0
  6. data/lib/sinatra/config_file.rb +151 -0
  7. data/lib/sinatra/content_for.rb +111 -0
  8. data/lib/sinatra/contrib.rb +39 -0
  9. data/lib/sinatra/contrib/all.rb +2 -0
  10. data/lib/sinatra/contrib/setup.rb +53 -0
  11. data/lib/sinatra/contrib/version.rb +45 -0
  12. data/lib/sinatra/cookies.rb +331 -0
  13. data/lib/sinatra/decompile.rb +113 -0
  14. data/lib/sinatra/engine_tracking.rb +96 -0
  15. data/lib/sinatra/extension.rb +95 -0
  16. data/lib/sinatra/json.rb +134 -0
  17. data/lib/sinatra/link_header.rb +132 -0
  18. data/lib/sinatra/multi_route.rb +81 -0
  19. data/lib/sinatra/namespace.rb +282 -0
  20. data/lib/sinatra/reloader.rb +384 -0
  21. data/lib/sinatra/respond_with.rb +245 -0
  22. data/lib/sinatra/streaming.rb +267 -0
  23. data/lib/sinatra/test_helpers.rb +87 -0
  24. data/sinatra-contrib.gemspec +125 -0
  25. data/spec/capture_spec.rb +80 -0
  26. data/spec/config_file/key_value.yml +6 -0
  27. data/spec/config_file/missing_env.yml +4 -0
  28. data/spec/config_file/with_envs.yml +7 -0
  29. data/spec/config_file/with_nested_envs.yml +11 -0
  30. data/spec/config_file_spec.rb +44 -0
  31. data/spec/content_for/different_key.erb +1 -0
  32. data/spec/content_for/different_key.erubis +1 -0
  33. data/spec/content_for/different_key.haml +2 -0
  34. data/spec/content_for/different_key.slim +2 -0
  35. data/spec/content_for/layout.erb +1 -0
  36. data/spec/content_for/layout.erubis +1 -0
  37. data/spec/content_for/layout.haml +1 -0
  38. data/spec/content_for/layout.slim +1 -0
  39. data/spec/content_for/multiple_blocks.erb +4 -0
  40. data/spec/content_for/multiple_blocks.erubis +4 -0
  41. data/spec/content_for/multiple_blocks.haml +8 -0
  42. data/spec/content_for/multiple_blocks.slim +8 -0
  43. data/spec/content_for/multiple_yields.erb +3 -0
  44. data/spec/content_for/multiple_yields.erubis +3 -0
  45. data/spec/content_for/multiple_yields.haml +3 -0
  46. data/spec/content_for/multiple_yields.slim +3 -0
  47. data/spec/content_for/passes_values.erb +1 -0
  48. data/spec/content_for/passes_values.erubis +1 -0
  49. data/spec/content_for/passes_values.haml +1 -0
  50. data/spec/content_for/passes_values.slim +1 -0
  51. data/spec/content_for/same_key.erb +1 -0
  52. data/spec/content_for/same_key.erubis +1 -0
  53. data/spec/content_for/same_key.haml +2 -0
  54. data/spec/content_for/same_key.slim +2 -0
  55. data/spec/content_for/takes_values.erb +1 -0
  56. data/spec/content_for/takes_values.erubis +1 -0
  57. data/spec/content_for/takes_values.haml +3 -0
  58. data/spec/content_for/takes_values.slim +3 -0
  59. data/spec/content_for_spec.rb +201 -0
  60. data/spec/cookies_spec.rb +782 -0
  61. data/spec/decompile_spec.rb +44 -0
  62. data/spec/extension_spec.rb +33 -0
  63. data/spec/json_spec.rb +115 -0
  64. data/spec/link_header_spec.rb +100 -0
  65. data/spec/multi_route_spec.rb +45 -0
  66. data/spec/namespace/foo.erb +1 -0
  67. data/spec/namespace/nested/foo.erb +1 -0
  68. data/spec/namespace_spec.rb +623 -0
  69. data/spec/okjson.rb +581 -0
  70. data/spec/reloader/app.rb.erb +40 -0
  71. data/spec/reloader_spec.rb +441 -0
  72. data/spec/respond_with/bar.erb +1 -0
  73. data/spec/respond_with/bar.json.erb +1 -0
  74. data/spec/respond_with/foo.html.erb +1 -0
  75. data/spec/respond_with/not_html.sass +2 -0
  76. data/spec/respond_with_spec.rb +289 -0
  77. data/spec/spec_helper.rb +6 -0
  78. data/spec/streaming_spec.rb +436 -0
  79. metadata +256 -0
@@ -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
@@ -0,0 +1,267 @@
1
+ require 'sinatra/base'
2
+ require 'eventmachine'
3
+ require 'backports'
4
+
5
+ module Sinatra
6
+
7
+ # = Sinatra::Streaming
8
+ #
9
+ # Sinatra 1.3 introduced the +stream+ helper. This addon improves the
10
+ # streaming API by making the stream object immitate an IO object, turing
11
+ # it into a real Deferrable and making the body play nicer with middleware
12
+ # unaware of streaming.
13
+ #
14
+ # == IO-like behavior
15
+ #
16
+ # This is useful when passing the stream object to a library expecting an
17
+ # IO or StringIO object.
18
+ #
19
+ # get '/' do
20
+ # stream do |out|
21
+ # out.puts "Hello World!", "How are you?"
22
+ # out.write "Written #{out.pos} bytes so far!\n"
23
+ # out.putc(65) unless out.closed?
24
+ # out.flush
25
+ # end
26
+ # end
27
+ #
28
+ # == Proper Deferrable
29
+ #
30
+ # Handy when using EventMachine.
31
+ #
32
+ # list = []
33
+ #
34
+ # get '/' do
35
+ # stream(false) do |out|
36
+ # list << out
37
+ # out.callback { list.delete out }
38
+ # out.errback do
39
+ # logger.warn "lost connection"
40
+ # list.delete out
41
+ # end
42
+ # end
43
+ # end
44
+ #
45
+ # == Better Middleware Handling
46
+ #
47
+ # Blocks passed to #map! or #map will actually be applied while streaming
48
+ # (as you might suspect, #map! applies modifications to the current body,
49
+ # #map creates a new one):
50
+ #
51
+ # class StupidMiddleware
52
+ # def initialize(app) @app = app end
53
+ #
54
+ # def call(env)
55
+ # status, headers, body = @app.call(env)
56
+ # body.map! { |e| e.upcase }
57
+ # [status, headers, body]
58
+ # end
59
+ # end
60
+ #
61
+ # use StupidMiddleware
62
+ #
63
+ # get '/' do
64
+ # stream do |out|
65
+ # out.puts "still"
66
+ # sleep 1
67
+ # out.puts "streaming"
68
+ # end
69
+ # end
70
+ #
71
+ # Even works if #each is used to generate an Enumerator:
72
+ #
73
+ # def call(env)
74
+ # status, headers, body = @app.call(env)
75
+ # body = body.each.map { |s| s.upcase }
76
+ # [status, headers, body]
77
+ # end
78
+ #
79
+ # Note that both examples violate the Rack specification.
80
+ #
81
+ # == Setup
82
+ #
83
+ # In a classic application:
84
+ #
85
+ # require "sinatra"
86
+ # require "sinatra/streaming"
87
+ #
88
+ # In a modular application:
89
+ #
90
+ # require "sinatra/base"
91
+ # require "sinatra/streaming"
92
+ #
93
+ # class MyApp < Sinatra::Base
94
+ # helpers Streaming
95
+ # end
96
+ module Streaming
97
+ def stream(*)
98
+ stream = super
99
+ stream.extend Stream
100
+ stream.app = self
101
+ env['async.close'].callback { stream.close } if env.key? 'async.close'
102
+ stream
103
+ end
104
+
105
+ module Stream
106
+ include EventMachine::Deferrable
107
+
108
+ attr_accessor :app, :lineno, :pos, :transformer, :closed
109
+ alias tell pos
110
+ alias closed? closed
111
+
112
+ def self.extended(obj)
113
+ obj.closed, obj.lineno, obj.pos = false, 0, 0
114
+ obj.callback { obj.closed = true }
115
+ obj.errback { obj.closed = true }
116
+ end
117
+
118
+ def <<(data)
119
+ raise IOError, 'not opened for writing' if closed?
120
+ data = data.to_s
121
+ data = @transformer[data] if @transformer
122
+ @pos += data.bytesize
123
+ super(data)
124
+ end
125
+
126
+ def each
127
+ # that way body.each.map { ... } works
128
+ return self unless block_given?
129
+ super
130
+ end
131
+
132
+ def map(&block)
133
+ # dup would not copy the mixin
134
+ clone.map!(&block)
135
+ end
136
+
137
+ def map!(&block)
138
+ if @transformer
139
+ inner, outer = @transformer, block
140
+ block = proc { |value| outer[inner[value]] }
141
+ end
142
+ @transformer = block
143
+ self
144
+ end
145
+
146
+ def write(data)
147
+ self << data
148
+ data.to_s.bytesize
149
+ end
150
+
151
+ alias syswrite write
152
+ alias write_nonblock write
153
+
154
+ def print(*args)
155
+ args.each { |arg| self << arg }
156
+ nil
157
+ end
158
+
159
+ def printf(format, *args)
160
+ print(format.to_s % args)
161
+ end
162
+
163
+ def putc(c)
164
+ print c.chr
165
+ end
166
+
167
+ def puts(*args)
168
+ args.each { |arg| self << "#{arg}\n" }
169
+ nil
170
+ end
171
+
172
+ def close
173
+ @scheduler.schedule { succeed }
174
+ nil
175
+ end
176
+
177
+ def close_read
178
+ raise IOError, "closing non-duplex IO for reading"
179
+ end
180
+
181
+ def closed_read?
182
+ true
183
+ end
184
+
185
+ def closed_write?
186
+ closed?
187
+ end
188
+
189
+ def external_encoding
190
+ Encoding.find settings.default_encoding
191
+ rescue NameError
192
+ settings.default_encoding
193
+ end
194
+
195
+ def closed?
196
+ @closed
197
+ end
198
+
199
+ def settings
200
+ app.settings
201
+ end
202
+
203
+ def rewind
204
+ @pos = @lineno = 0
205
+ end
206
+
207
+ def not_open_for_reading(*)
208
+ raise IOError, "not opened for reading"
209
+ end
210
+
211
+ alias bytes not_open_for_reading
212
+ alias eof? not_open_for_reading
213
+ alias eof not_open_for_reading
214
+ alias getbyte not_open_for_reading
215
+ alias getc not_open_for_reading
216
+ alias gets not_open_for_reading
217
+ alias read not_open_for_reading
218
+ alias read_nonblock not_open_for_reading
219
+ alias readbyte not_open_for_reading
220
+ alias readchar not_open_for_reading
221
+ alias readline not_open_for_reading
222
+ alias readlines not_open_for_reading
223
+ alias readpartial not_open_for_reading
224
+ alias sysread not_open_for_reading
225
+ alias ungetbyte not_open_for_reading
226
+ alias ungetc not_open_for_reading
227
+ private :not_open_for_reading
228
+
229
+ def enum_not_open_for_reading(*)
230
+ not_open_for_reading if block_given?
231
+ enum_for(:not_open_for_reading)
232
+ end
233
+
234
+ alias chars enum_not_open_for_reading
235
+ alias each_line enum_not_open_for_reading
236
+ alias each_byte enum_not_open_for_reading
237
+ alias each_char enum_not_open_for_reading
238
+ alias lines enum_not_open_for_reading
239
+ undef enum_not_open_for_reading
240
+
241
+ def dummy(*) end
242
+ alias flush dummy
243
+ alias fsync dummy
244
+ alias internal_encoding dummy
245
+ alias pid dummy
246
+ undef dummy
247
+
248
+ def seek(*)
249
+ 0
250
+ end
251
+
252
+ alias sysseek seek
253
+
254
+ def sync
255
+ true
256
+ end
257
+
258
+ def tty?
259
+ false
260
+ end
261
+
262
+ alias isatty tty?
263
+ end
264
+ end
265
+
266
+ helpers Streaming
267
+ end