rubycut-sinatra-contrib 1.4.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.
Files changed (81) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +136 -0
  3. data/Rakefile +75 -0
  4. data/ideas.md +29 -0
  5. data/lib/sinatra/capture.rb +124 -0
  6. data/lib/sinatra/config_file.rb +167 -0
  7. data/lib/sinatra/content_for.rb +125 -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 +17 -0
  12. data/lib/sinatra/cookies.rb +331 -0
  13. data/lib/sinatra/decompile.rb +120 -0
  14. data/lib/sinatra/engine_tracking.rb +96 -0
  15. data/lib/sinatra/extension.rb +95 -0
  16. data/lib/sinatra/json.rb +130 -0
  17. data/lib/sinatra/link_header.rb +132 -0
  18. data/lib/sinatra/multi_route.rb +87 -0
  19. data/lib/sinatra/namespace.rb +284 -0
  20. data/lib/sinatra/reloader.rb +394 -0
  21. data/lib/sinatra/respond_with.rb +249 -0
  22. data/lib/sinatra/streaming.rb +267 -0
  23. data/lib/sinatra/test_helpers.rb +87 -0
  24. data/sinatra-contrib.gemspec +127 -0
  25. data/spec/capture_spec.rb +93 -0
  26. data/spec/config_file/key_value.yml +6 -0
  27. data/spec/config_file/key_value.yml.erb +6 -0
  28. data/spec/config_file/key_value_override.yml +2 -0
  29. data/spec/config_file/missing_env.yml +4 -0
  30. data/spec/config_file/with_envs.yml +7 -0
  31. data/spec/config_file/with_nested_envs.yml +11 -0
  32. data/spec/config_file_spec.rb +63 -0
  33. data/spec/content_for/different_key.erb +1 -0
  34. data/spec/content_for/different_key.erubis +1 -0
  35. data/spec/content_for/different_key.haml +2 -0
  36. data/spec/content_for/different_key.slim +2 -0
  37. data/spec/content_for/layout.erb +1 -0
  38. data/spec/content_for/layout.erubis +1 -0
  39. data/spec/content_for/layout.haml +1 -0
  40. data/spec/content_for/layout.slim +1 -0
  41. data/spec/content_for/multiple_blocks.erb +4 -0
  42. data/spec/content_for/multiple_blocks.erubis +4 -0
  43. data/spec/content_for/multiple_blocks.haml +8 -0
  44. data/spec/content_for/multiple_blocks.slim +8 -0
  45. data/spec/content_for/multiple_yields.erb +3 -0
  46. data/spec/content_for/multiple_yields.erubis +3 -0
  47. data/spec/content_for/multiple_yields.haml +3 -0
  48. data/spec/content_for/multiple_yields.slim +3 -0
  49. data/spec/content_for/passes_values.erb +1 -0
  50. data/spec/content_for/passes_values.erubis +1 -0
  51. data/spec/content_for/passes_values.haml +1 -0
  52. data/spec/content_for/passes_values.slim +1 -0
  53. data/spec/content_for/same_key.erb +1 -0
  54. data/spec/content_for/same_key.erubis +1 -0
  55. data/spec/content_for/same_key.haml +2 -0
  56. data/spec/content_for/same_key.slim +2 -0
  57. data/spec/content_for/takes_values.erb +1 -0
  58. data/spec/content_for/takes_values.erubis +1 -0
  59. data/spec/content_for/takes_values.haml +3 -0
  60. data/spec/content_for/takes_values.slim +3 -0
  61. data/spec/content_for_spec.rb +213 -0
  62. data/spec/cookies_spec.rb +802 -0
  63. data/spec/decompile_spec.rb +44 -0
  64. data/spec/extension_spec.rb +33 -0
  65. data/spec/json_spec.rb +117 -0
  66. data/spec/link_header_spec.rb +100 -0
  67. data/spec/multi_route_spec.rb +60 -0
  68. data/spec/namespace/foo.erb +1 -0
  69. data/spec/namespace/nested/foo.erb +1 -0
  70. data/spec/namespace_spec.rb +676 -0
  71. data/spec/okjson.rb +581 -0
  72. data/spec/reloader/app.rb.erb +40 -0
  73. data/spec/reloader_spec.rb +441 -0
  74. data/spec/respond_with/bar.erb +1 -0
  75. data/spec/respond_with/bar.json.erb +1 -0
  76. data/spec/respond_with/foo.html.erb +1 -0
  77. data/spec/respond_with/not_html.sass +2 -0
  78. data/spec/respond_with_spec.rb +297 -0
  79. data/spec/spec_helper.rb +7 -0
  80. data/spec/streaming_spec.rb +436 -0
  81. metadata +313 -0
@@ -0,0 +1,249 @@
1
+ require 'sinatra/json'
2
+ require 'sinatra/base'
3
+
4
+ module Sinatra
5
+ #
6
+ # = Sinatra::RespondWith
7
+ #
8
+ # These extensions let 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 severe as those might appear in the first place: keep in
57
+ # mind that only known file extensions are used). You 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 an app global (class method) `respond_to`, that lets 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(method, *args, &block)
122
+ return super if args.any? or block.nil? or not @app.mime_type(method)
123
+ on(method, &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
+
142
+ renderer = args.first
143
+ options = args[1..-1] + [{:locals => locals}]
144
+
145
+ halt send(renderer, *options)
146
+ end
147
+ end
148
+ if object
149
+ exts.each do |ext|
150
+ halt json(object) if ext == :json
151
+ next unless object.respond_to? method = "to_#{ext}"
152
+ halt(*object.send(method))
153
+ end
154
+ end
155
+ false
156
+ end
157
+ format.finish(&block)
158
+ end
159
+
160
+ def respond_to(&block)
161
+ Format.new(self).finish(&block)
162
+ end
163
+
164
+ private
165
+
166
+ def template_for(name, exts)
167
+ # in production this is cached, so don't worry too much about runtime
168
+ possible = []
169
+ settings.template_engines[:all].each do |engine|
170
+ exts.each { |ext| possible << [engine, "#{name}.#{ext}"] }
171
+ end
172
+ exts.each do |ext|
173
+ settings.template_engines[ext].each { |e| possible << [e, name] }
174
+ end
175
+ possible.each do |engine, template|
176
+ # not exactly like Tilt[engine], but does not trigger a require
177
+ klass = Tilt.mappings[Tilt.normalize(engine)].first
178
+ find_template(settings.views, template, klass) do |file|
179
+ next unless File.exist? file
180
+ return settings.rendering_method(engine) << template.to_sym
181
+ end
182
+ end
183
+ [] # nil or false would not be cached
184
+ end
185
+ end
186
+
187
+ attr_accessor :ext_map
188
+
189
+ def remap_extensions
190
+ ext_map.clear
191
+ Rack::Mime::MIME_TYPES.each { |e,t| ext_map[t] << e[1..-1].to_sym }
192
+ ext_map['text/javascript'] << 'js'
193
+ ext_map['text/xml'] << 'xml'
194
+ end
195
+
196
+ def mime_type(*)
197
+ result = super
198
+ remap_extensions
199
+ result
200
+ end
201
+
202
+ def respond_to(*formats)
203
+ if formats.any?
204
+ @respond_to ||= []
205
+ @respond_to.concat formats
206
+ elsif @respond_to.nil? and superclass.respond_to? :respond_to
207
+ superclass.respond_to
208
+ else
209
+ @respond_to
210
+ end
211
+ end
212
+
213
+ def rendering_method(engine)
214
+ return [engine] if Sinatra::Templates.method_defined? engine
215
+ return [:mab] if engine.to_sym == :markaby
216
+ [:render, :engine]
217
+ end
218
+
219
+ private
220
+
221
+ def compile!(verb, path, block, options = {})
222
+ options[:provides] ||= respond_to if respond_to
223
+ super
224
+ end
225
+
226
+ ENGINES = {
227
+ :css => [:less, :sass, :scss],
228
+ :xml => [:builder, :nokogiri],
229
+ :js => [:coffee],
230
+ :json => [:yajl],
231
+ :html => [:erb, :erubis, :haml, :slim, :liquid, :radius, :mab, :markdown,
232
+ :textile, :rdoc],
233
+ :all => Sinatra::Templates.instance_methods.map(&:to_sym) + [:mab] -
234
+ [:find_template, :markaby]
235
+ }
236
+
237
+ ENGINES.default = []
238
+
239
+ def self.registered(base)
240
+ base.ext_map = Hash.new { |h,k| h[k] = [] }
241
+ base.set :template_engines, ENGINES.dup
242
+ base.remap_extensions
243
+ base.helpers Helpers
244
+ end
245
+ end
246
+
247
+ register RespondWith
248
+ Delegator.delegate :respond_to
249
+ 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, turning
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(:keep_open) 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 when streaming
48
+ # takes place (as you might have suspected, #map! applies modifications
49
+ # to the current body, while #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 Sinatra::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