deas 0.38.0 → 0.39.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/Gemfile +2 -1
  2. data/{LICENSE.txt → LICENSE} +0 -0
  3. data/deas.gemspec +4 -3
  4. data/lib/deas/deas_runner.rb +17 -12
  5. data/lib/deas/handler_proxy.rb +14 -8
  6. data/lib/deas/redirect_proxy.rb +12 -12
  7. data/lib/deas/runner.rb +169 -19
  8. data/lib/deas/server.rb +14 -10
  9. data/lib/deas/template_source.rb +2 -2
  10. data/lib/deas/test_runner.rb +59 -60
  11. data/lib/deas/version.rb +1 -1
  12. data/lib/deas/view_handler.rb +55 -30
  13. data/test/support/empty_view_handler.rb +7 -0
  14. data/test/support/factory.rb +5 -0
  15. data/test/support/fake_request.rb +29 -0
  16. data/test/support/fake_sinatra_call.rb +11 -16
  17. data/test/support/file1.txt +1 -0
  18. data/test/support/file2.txt +1 -0
  19. data/test/support/routes.rb +7 -7
  20. data/test/support/show.html +0 -0
  21. data/test/support/show.json +0 -0
  22. data/test/system/{rack_tests.rb → deas_tests.rb} +19 -14
  23. data/test/unit/deas_runner_tests.rb +209 -20
  24. data/test/unit/handler_proxy_tests.rb +27 -19
  25. data/test/unit/redirect_proxy_tests.rb +32 -33
  26. data/test/unit/respond_with_proxy_tests.rb +1 -2
  27. data/test/unit/route_proxy_tests.rb +1 -1
  28. data/test/unit/route_tests.rb +1 -1
  29. data/test/unit/router_tests.rb +5 -5
  30. data/test/unit/runner_tests.rb +619 -76
  31. data/test/unit/server_tests.rb +6 -1
  32. data/test/unit/sinatra_app_tests.rb +1 -1
  33. data/test/unit/test_runner_tests.rb +377 -106
  34. data/test/unit/url_tests.rb +1 -1
  35. data/test/unit/view_handler_tests.rb +325 -182
  36. metadata +43 -24
  37. data/lib/deas/sinatra_runner.rb +0 -55
  38. data/lib/deas/test_helpers.rb +0 -23
  39. data/test/support/view_handlers.rb +0 -83
  40. data/test/unit/sinatra_runner_tests.rb +0 -79
  41. data/test/unit/test_helpers_tests.rb +0 -53
data/Gemfile CHANGED
@@ -3,5 +3,6 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rake', "~> 10.4.0"
6
- gem 'pry', "~> 0.9.0"
6
+ gem 'pry', "~> 0.9.0"
7
+
7
8
  gem 'rack-test'
File without changes
@@ -18,9 +18,10 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency("ns-options", ["~> 1.1", ">= 1.1.4"])
22
- gem.add_dependency("rack", ["~> 1.5"])
23
- gem.add_dependency("sinatra", ["~> 1.4"])
21
+ gem.add_dependency("much-plugin", ["~> 0.1"])
22
+ gem.add_dependency("ns-options", ["~> 1.1", ">= 1.1.4"])
23
+ gem.add_dependency("rack", ["~> 1.5"])
24
+ gem.add_dependency("sinatra", ["~> 1.4"])
24
25
 
25
26
  gem.add_development_dependency("assert", ["~> 2.15"])
26
27
  gem.add_development_dependency("assert-rack-test")
@@ -1,3 +1,4 @@
1
+ require 'rack/utils'
1
2
  require 'deas/runner'
2
3
 
3
4
  module Deas
@@ -5,25 +6,29 @@ module Deas
5
6
  class DeasRunner < Runner
6
7
 
7
8
  def initialize(handler_class, args = nil)
8
- a = args || {}
9
- runner_args = a.merge(:params => NormalizedParams.new(a[:params]).value)
10
- super(handler_class, runner_args)
9
+ args ||= {}
10
+ super(
11
+ handler_class,
12
+ args.merge(:params => NormalizedParams.new(args[:params]).value)
13
+ )
11
14
  end
12
15
 
13
16
  def run
14
- run_callbacks self.handler_class.before_callbacks
15
- self.handler.init
16
- response_data = self.handler.run
17
- run_callbacks self.handler_class.after_callbacks
18
- response_data
17
+ catch(:halt) do
18
+ self.handler.deas_run_callback 'before'
19
+ catch(:halt){ self.handler.deas_init; self.handler.deas_run }
20
+ self.handler.deas_run_callback 'after'
21
+ end
22
+
23
+ self.to_rack.tap do |(status, headers, body)|
24
+ headers['Content-Length'] ||= body.inject(0) do |length, part|
25
+ length + Rack::Utils.bytesize(part)
26
+ end.to_s
27
+ end
19
28
  end
20
29
 
21
30
  private
22
31
 
23
- def run_callbacks(callbacks)
24
- callbacks.each{|proc| self.handler.instance_eval(&proc) }
25
- end
26
-
27
32
  class NormalizedParams < Deas::Runner::NormalizedParams
28
33
  def file_type?(value)
29
34
  value.kind_of?(::Tempfile)
@@ -1,5 +1,5 @@
1
1
  require 'deas/exceptions'
2
- require 'deas/sinatra_runner'
2
+ require 'deas/deas_runner'
3
3
 
4
4
  module Deas
5
5
 
@@ -16,15 +16,21 @@ module Deas
16
16
  end
17
17
 
18
18
  def run(server_data, sinatra_call)
19
- runner = SinatraRunner.new(self.handler_class, {
20
- :sinatra_call => sinatra_call,
21
- :request => sinatra_call.request,
22
- :response => sinatra_call.response,
23
- :session => sinatra_call.session,
24
- :params => sinatra_call.params,
19
+ # these are not part of Deas' intended behavior and route matching
20
+ # they are side-effects of using Sinatra. remove them so they won't
21
+ # be relied upon in Deas apps.
22
+ sinatra_call.params.delete(:splat)
23
+ sinatra_call.params.delete('splat')
24
+ sinatra_call.params.delete(:captures)
25
+ sinatra_call.params.delete('captures')
26
+
27
+ runner = DeasRunner.new(self.handler_class, {
25
28
  :logger => server_data.logger,
26
29
  :router => server_data.router,
27
- :template_source => server_data.template_source
30
+ :template_source => server_data.template_source,
31
+ :request => sinatra_call.request,
32
+ :session => sinatra_call.session,
33
+ :params => sinatra_call.params
28
34
  })
29
35
 
30
36
  runner.request.env.tap do |env|
@@ -8,7 +8,7 @@ module Deas
8
8
 
9
9
  attr_reader :handler_class_name, :handler_class
10
10
 
11
- def initialize(router, path = nil, &block)
11
+ def initialize(router, location = nil, &block)
12
12
  @handler_class = Class.new do
13
13
  include Deas::ViewHandler
14
14
 
@@ -17,34 +17,34 @@ module Deas
17
17
  @router = value
18
18
  end
19
19
 
20
- def self.redirect_path; @redirect_path; end
21
- def self.redirect_path=(value)
22
- @redirect_path = value
20
+ def self.redirect_location; @redirect_location; end
21
+ def self.redirect_location=(value)
22
+ @redirect_location = value
23
23
  end
24
24
 
25
25
  def self.name; 'Deas::RedirectHandler'; end
26
26
 
27
- attr_reader :redirect_path
27
+ attr_reader :redirect_location
28
28
 
29
29
  def init!
30
- @redirect_path = self.class.router.prepend_base_url(
31
- self.instance_eval(&self.class.redirect_path)
30
+ @redirect_location = self.class.router.prepend_base_url(
31
+ self.instance_eval(&self.class.redirect_location)
32
32
  )
33
33
  end
34
34
 
35
35
  def run!
36
- redirect @redirect_path
36
+ redirect @redirect_location
37
37
  end
38
38
 
39
39
  end
40
40
 
41
41
  @handler_class.router = router
42
- @handler_class.redirect_path = if path.nil?
42
+ @handler_class.redirect_location = if location.nil?
43
43
  block
44
- elsif path.kind_of?(Deas::Url)
45
- proc{ path.path_for(params) }
44
+ elsif location.kind_of?(Deas::Url)
45
+ proc{ location.path_for(params) }
46
46
  else
47
- proc{ path }
47
+ proc{ location }
48
48
  end
49
49
  @handler_class_name = @handler_class.name
50
50
  end
@@ -1,4 +1,5 @@
1
1
  require 'rack/utils'
2
+ require 'pathname'
2
3
  require 'deas/logger'
3
4
  require 'deas/router'
4
5
  require 'deas/template_source'
@@ -7,40 +8,115 @@ module Deas
7
8
 
8
9
  class Runner
9
10
 
11
+ DEFAULT_MIME_TYPE = 'application/octet-stream'.freeze
12
+ DEFAULT_CHARSET = 'utf-8'.freeze
13
+ DEFAULT_STATUS = 200.freeze
14
+ DEFAULT_BODY = [].freeze
15
+
10
16
  attr_reader :handler_class, :handler
11
- attr_reader :request, :response, :session
12
- attr_reader :params, :logger, :router, :template_source
17
+ attr_reader :logger, :router, :template_source
18
+ attr_reader :request, :session, :params
13
19
 
14
20
  def initialize(handler_class, args = nil)
21
+ @status, @headers, @body = nil, Rack::Utils::HeaderHash.new, nil
22
+
23
+ args ||= {}
24
+ @logger = args[:logger] || Deas::NullLogger.new
25
+ @router = args[:router] || Deas::Router.new
26
+ @template_source = args[:template_source] || Deas::NullTemplateSource.new
27
+ @request = args[:request]
28
+ @session = args[:session]
29
+ @params = args[:params] || {}
30
+
15
31
  @handler_class = handler_class
16
32
  @handler = @handler_class.new(self)
33
+ end
34
+
35
+ def run
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def to_rack
40
+ [self.status || DEFAULT_STATUS, self.headers.to_hash, self.body || DEFAULT_BODY]
41
+ end
17
42
 
18
- a = args || {}
19
- @request = a[:request]
20
- @response = a[:response]
21
- @session = a[:session]
22
- @params = a[:params] || {}
23
- @logger = a[:logger] || Deas::NullLogger.new
24
- @router = a[:router] || Deas::Router.new
25
- @template_source = a[:template_source] || Deas::NullTemplateSource.new
43
+ def status(value = nil)
44
+ @status = value if !value.nil?
45
+ @status
26
46
  end
27
47
 
28
- def halt(*args); raise NotImplementedError; end
29
- def redirect(*args); raise NotImplementedError; end
30
- def content_type(*args); raise NotImplementedError; end
31
- def status(*args); raise NotImplementedError; end
32
- def headers(*args); raise NotImplementedError; end
33
- def send_file(*args); raise NotImplementedError; end
48
+ def headers(value = nil)
49
+ @headers.merge!(value) if !value.nil?
50
+ @headers
51
+ end
34
52
 
35
- # the render methods are used by both the deas and test runners
36
- # so we implement here
53
+ def body(value = nil)
54
+ if !value.nil?
55
+ unless value.respond_to?(:each)
56
+ raise ArgumentError, "this body value (class `#{value.class}`), does not "\
57
+ "respond to `each`."
58
+ end
59
+ @body = value
60
+ end
61
+ @body
62
+ end
63
+
64
+ def content_type(extname, params = nil)
65
+ self.headers['Content-Type'] = get_content_type(extname, params)
66
+ end
67
+
68
+ def halt(*args)
69
+ self.status(args.shift) if args.first.instance_of?(::Fixnum)
70
+ self.headers(args.shift) if args.first.kind_of?(::Hash)
71
+ self.body(args.shift) if args.first.respond_to?(:each)
72
+ throw :halt
73
+ end
74
+
75
+ def redirect(location, *halt_args)
76
+ self.status(302)
77
+ self.headers['Location'] = get_absolute_url(location)
78
+ halt(*halt_args)
79
+ end
80
+
81
+ def send_file(file_path, opts = nil)
82
+ path_name = Pathname.new(file_path)
83
+ self.halt(404, []) if !path_name.exist?
84
+
85
+ env = self.request.env
86
+ mtime = path_name.mtime.httpdate.to_s
87
+ self.halt(304, []) if env['HTTP_IF_MODIFIED_SINCE'] == mtime
88
+ self.headers['Last-Modified'] ||= mtime
89
+
90
+ self.headers['Content-Type'] ||= get_content_type(path_name.extname)
91
+
92
+ opts ||= {}
93
+ disposition = opts[:disposition]
94
+ filename = opts[:filename]
95
+ disposition ||= 'attachment' if !filename.nil?
96
+ filename ||= path_name.basename
97
+ if !disposition.nil?
98
+ self.headers['Content-Disposition'] ||= "#{disposition};filename=\"#{filename}\""
99
+ end
100
+
101
+ sfb = SendFileBody.new(env, path_name)
102
+ self.body(sfb)
103
+ self.headers['Content-Length'] ||= sfb.size.to_s
104
+ self.headers['Content-Range'] ||= sfb.content_range if sfb.partial?
105
+ self.status(sfb.partial? ? 206 : 200)
106
+
107
+ self.halt # be consistent with halts above - `send_file` always halts
108
+ end
37
109
 
38
110
  def render(template_name, locals = nil)
39
111
  source_render(self.template_source, template_name, locals)
40
112
  end
41
113
 
42
114
  def source_render(source, template_name, locals = nil)
43
- source.render(template_name, self.handler, locals || {})
115
+ self.headers['Content-Type'] ||= get_content_type(
116
+ File.extname(template_name),
117
+ 'charset' => DEFAULT_CHARSET
118
+ )
119
+ self.body(source.render(template_name, self.handler, locals || {}))
44
120
  end
45
121
 
46
122
  def partial(template_name, locals = nil)
@@ -51,6 +127,19 @@ module Deas
51
127
  source.partial(template_name, locals || {})
52
128
  end
53
129
 
130
+ private
131
+
132
+ def get_content_type(extname, params = nil)
133
+ [ Rack::Mime.mime_type(extname, DEFAULT_MIME_TYPE),
134
+ params ? params.map{ |k,v| "#{k}=#{v}" }.join(',') : nil
135
+ ].compact.join(';')
136
+ end
137
+
138
+ def get_absolute_url(url)
139
+ return url if url =~ /\A[A-z][A-z0-9\+\.\-]*:/
140
+ File.join("#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}", url)
141
+ end
142
+
54
143
  class NormalizedParams
55
144
 
56
145
  attr_reader :value
@@ -73,6 +162,67 @@ module Deas
73
162
 
74
163
  end
75
164
 
165
+ class SendFileBody
166
+
167
+ # this class borrows from the body range handling in Rack::File.
168
+
169
+ CHUNK_SIZE = (8*1024).freeze # 8k
170
+
171
+ attr_reader :path_name, :size, :content_range
172
+
173
+ def initialize(env, path_name)
174
+ @path_name = path_name
175
+
176
+ file_size = @path_name.size? || Rack::Utils.bytesize(path_name.read)
177
+ ranges = Rack::Utils.byte_ranges(env, file_size)
178
+ if ranges.nil? || ranges.empty? || ranges.length > 1
179
+ # No ranges or multiple ranges are not supported
180
+ @range = 0..file_size-1
181
+ @content_range = nil
182
+ else
183
+ # single range
184
+ @range = ranges[0]
185
+ @content_range = "bytes #{@range.begin}-#{@range.end}/#{file_size}"
186
+ end
187
+
188
+ @size = self.range_end - self.range_begin + 1
189
+ end
190
+
191
+ def partial?
192
+ !@content_range.nil?
193
+ end
194
+
195
+ def range_begin; @range.begin; end
196
+ def range_end; @range.end; end
197
+
198
+ def each
199
+ @path_name.open("rb") do |io|
200
+ io.seek(@range.begin)
201
+ remaining_len = self.size
202
+ while remaining_len > 0
203
+ part = io.read([CHUNK_SIZE, remaining_len].min)
204
+ break if part.nil?
205
+
206
+ remaining_len -= part.length
207
+ yield part
208
+ end
209
+ end
210
+ end
211
+
212
+ def inspect
213
+ "#<#{self.class}:#{'0x0%x' % (self.object_id << 1)} " \
214
+ "path=#{self.path_name} " \
215
+ "range_begin=#{self.range_begin} range_end=#{self.range_end}>"
216
+ end
217
+
218
+ def ==(other_body)
219
+ self.path_name.to_s == other_body.path_name.to_s &&
220
+ self.range_begin == other_body.range_begin &&
221
+ self.range_end == other_body.range_end
222
+ end
223
+
224
+ end
225
+
76
226
  end
77
227
 
78
228
  end
@@ -1,6 +1,7 @@
1
- require 'pathname'
1
+ require 'much-plugin'
2
2
  require 'ns-options'
3
3
  require 'ns-options/boolean'
4
+ require 'pathname'
4
5
  require 'deas/exceptions'
5
6
  require 'deas/logger'
6
7
  require 'deas/logging'
@@ -12,19 +13,20 @@ require 'deas/template_source'
12
13
  module Deas
13
14
 
14
15
  module Server
16
+ include MuchPlugin
15
17
 
16
- def self.included(receiver)
17
- receiver.class_eval do
18
- extend ClassMethods
19
- include InstanceMethods
20
- end
18
+ plugin_included do
19
+ extend ClassMethods
20
+ include InstanceMethods
21
21
  end
22
22
 
23
23
  module InstanceMethods
24
+
24
25
  # TODO: once Deas is no longer powered by Sinatra, this should define an
25
26
  # `initialize` method that builds a server instance. Right now there is
26
27
  # a `new` class method that builds a SinatraApp which does this init
27
28
  # behavior
29
+
28
30
  end
29
31
 
30
32
  module ClassMethods
@@ -174,8 +176,8 @@ module Deas
174
176
  # server handling options
175
177
 
176
178
  option :verbose_logging, NsOptions::Boolean, :default => true
177
- option :logger, :default => proc{ Deas::NullLogger.new }
178
- option :template_source, :default => proc{ Deas::NullTemplateSource.new }
179
+ option :logger
180
+ option :template_source
179
181
 
180
182
  attr_accessor :settings, :init_procs, :error_procs, :template_helpers
181
183
  attr_accessor :middlewares, :router
@@ -186,8 +188,10 @@ module Deas
186
188
  # Configuration class `root`, which will not update these options as
187
189
  # expected.
188
190
  super((values || {}).merge({
189
- :public_root => proc{ self.root.join('public') },
190
- :views_root => proc{ self.root.join('views') }
191
+ :public_root => proc{ self.root.join('public') },
192
+ :views_root => proc{ self.root.join('views') },
193
+ :logger => proc{ Deas::NullLogger.new },
194
+ :template_source => proc{ Deas::NullTemplateSource.new(self.root) }
191
195
  }))
192
196
  @settings = {}
193
197
  @init_procs, @error_procs, @template_helpers, @middlewares = [], [], [], []
@@ -75,8 +75,8 @@ module Deas
75
75
 
76
76
  class NullTemplateSource < TemplateSource
77
77
 
78
- def initialize
79
- super('')
78
+ def initialize(root = nil)
79
+ super(root || '')
80
80
  end
81
81
 
82
82
  end