deas 0.38.0 → 0.39.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 (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