serverside 0.2.9 → 0.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.
Files changed (45) hide show
  1. data/CHANGELOG +56 -0
  2. data/Rakefile +12 -52
  3. data/bin/serverside +1 -1
  4. data/lib/serverside/application.rb +2 -1
  5. data/lib/serverside/caching.rb +62 -50
  6. data/lib/serverside/controllers.rb +91 -0
  7. data/lib/serverside/core_ext.rb +6 -0
  8. data/lib/serverside/daemon.rb +25 -5
  9. data/lib/serverside/request.rb +17 -11
  10. data/lib/serverside/routing.rb +11 -10
  11. data/lib/serverside/server.rb +14 -6
  12. data/lib/serverside/static.rb +7 -18
  13. data/lib/serverside/template.rb +20 -12
  14. data/spec/caching_spec.rb +318 -0
  15. data/spec/cluster_spec.rb +140 -0
  16. data/{test/spec → spec}/connection_spec.rb +4 -4
  17. data/{test/spec/controller_spec.rb → spec/controllers_spec.rb} +15 -12
  18. data/{test/spec → spec}/core_ext_spec.rb +23 -18
  19. data/spec/daemon_spec.rb +99 -0
  20. data/{test/spec → spec}/request_spec.rb +45 -45
  21. data/spec/routing_spec.rb +240 -0
  22. data/spec/server_spec.rb +40 -0
  23. data/spec/static_spec.rb +279 -0
  24. data/spec/template_spec.rb +129 -0
  25. metadata +21 -35
  26. data/lib/serverside/controller.rb +0 -67
  27. data/test/functional/primitive_static_server_test.rb +0 -61
  28. data/test/functional/request_body_test.rb +0 -93
  29. data/test/functional/routing_server.rb +0 -14
  30. data/test/functional/routing_server_test.rb +0 -41
  31. data/test/functional/static_profile.rb +0 -17
  32. data/test/functional/static_rfuzz.rb +0 -31
  33. data/test/functional/static_server.rb +0 -7
  34. data/test/functional/static_server_test.rb +0 -31
  35. data/test/spec/caching_spec.rb +0 -139
  36. data/test/test_helper.rb +0 -2
  37. data/test/unit/cluster_test.rb +0 -129
  38. data/test/unit/connection_test.rb +0 -48
  39. data/test/unit/core_ext_test.rb +0 -46
  40. data/test/unit/daemon_test.rb +0 -75
  41. data/test/unit/request_test.rb +0 -278
  42. data/test/unit/routing_test.rb +0 -171
  43. data/test/unit/server_test.rb +0 -28
  44. data/test/unit/static_test.rb +0 -171
  45. data/test/unit/template_test.rb +0 -78
data/CHANGELOG CHANGED
@@ -1,3 +1,59 @@
1
+ ==0.3.0
2
+
3
+ * Disabled cluster_spec and parts of daemon_spec for now due to strange forking behavior.
4
+
5
+ * Removed static file cache from Static.
6
+
7
+ * Fixed daemon_spec to work correctly.
8
+
9
+ * Removed all tests, moved specs to /spec, and fixed rakefile.
10
+
11
+ * Converted cluster_test to cluster_spec.
12
+
13
+ * Converted daemon_test to daemon_spec.
14
+
15
+ * Updated specs to work with RSpec 0.7.
16
+
17
+ * Changed rake spec task to do coverage as well.
18
+
19
+ * Renamed controller.rb to controllers.rb.
20
+
21
+ * Added String.underscore method.
22
+
23
+ * Renamed ServerSide::Controller.process to response.
24
+
25
+ * Improved HTTP caching code to better conform to the HTTP spec. Also improved the validate_cache method to work without a block as well.
26
+
27
+ * Removed unit tests that were converted to specs.
28
+
29
+ * Router now excepts handlers to return non-nil if the request was handled. Otherwise, the request goes to the next handler with an appropriate rule.
30
+
31
+ * Improved spec coverage for caching.
32
+
33
+ * Wrote Server spec.
34
+
35
+ * Wrote Router spec.
36
+
37
+ * Refactored HTTP::Server to allow creating instances without starting them.
38
+
39
+ * Fixed Router.routes_defined? to return non-nil if default route is defined.
40
+
41
+ * Renamed Router.has_routes? to routes_defined?.
42
+
43
+ * Fixed Static.serve_template and Static.serve_static to work correctly (render templates.)
44
+
45
+ * Removed deprecated code in Template.render.
46
+
47
+ * Wrote Static spec.
48
+
49
+ * Fixed bug in serverside script - wrong call to route_default instead of default_route.
50
+
51
+ * Refactored ServerSide::Template and wrote template spec.
52
+
53
+ * Added documentation to Controller.
54
+
55
+ * Fixed Controller.mount to build a self.inherited method, and do the routing there.
56
+
1
57
  ==0.2.9
2
58
 
3
59
  * Improved rake clean task.
data/Rakefile CHANGED
@@ -2,12 +2,11 @@ require 'rake'
2
2
  require 'rake/clean'
3
3
  require 'rake/gempackagetask'
4
4
  require 'rake/rdoctask'
5
- require 'rake/testtask'
6
5
  require 'fileutils'
7
6
  include FileUtils
8
7
 
9
8
  NAME = "serverside"
10
- VERS = "0.2.9"
9
+ VERS = "0.3.0"
11
10
  CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
12
11
  RDOC_OPTS = ['--quiet', '--title', "ServerSide Documentation",
13
12
  "--opname", "index.html",
@@ -47,7 +46,7 @@ spec = Gem::Specification.new do |s|
47
46
  s.add_dependency('metaid')
48
47
  s.required_ruby_version = '>= 1.8.2'
49
48
 
50
- s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,test,lib}/**/*")
49
+ s.files = %w(COPYING README Rakefile) + Dir.glob("{bin,doc,spec,lib}/**/*")
51
50
 
52
51
  s.require_path = "lib"
53
52
  s.bindir = "bin"
@@ -70,60 +69,23 @@ end
70
69
  desc 'Update docs and upload to rubyforge.org'
71
70
  task :doc_rforge do
72
71
  sh %{rake doc}
73
- sh %{scp -r doc/rdoc/* ciconia@rubyforge.org:/var/www/gforge-projects/serverside}
74
- end
75
-
76
- desc 'Run unit tests'
77
- Rake::TestTask.new('test_unit') do |t|
78
- t.libs << 'test'
79
- t.pattern = 'test/unit/*_test.rb'
80
- t.verbose = true
81
- end
82
-
83
- desc 'Run functional tests'
84
- Rake::TestTask.new('test_functional') do |t|
85
- t.libs << 'test'
86
- t.pattern = 'test/functional/*_test.rb'
87
- t.verbose = true
88
- end
89
-
90
- desc 'Run specification tests'
91
- task :spec do
92
- sh %{spec test/spec/*_spec.rb}
93
- end
94
-
95
- desc 'Run rcov'
96
- task :rcov do
97
- sh %{rcov test/unit/*_test.rb test/functional/*_test.rb}
98
- end
99
-
100
- desc 'Run all tests'
101
- Rake::TestTask.new('test') do |t|
102
- t.libs << 'test'
103
- t.pattern = 'test/**/*_test.rb'
104
- t.verbose = true
105
- end
106
-
107
- desc 'Run all tests, specs and finish with rcov'
108
- task :aok do
109
- sh %{rake rcov}
110
- sh %{rake spec}
72
+ sh %{scp -r doc/* ciconia@rubyforge.org:/var/www/gforge-projects/serverside}
111
73
  end
112
74
 
113
75
  require 'spec/rake/spectask'
114
76
 
115
- desc "Run specs RCov"
116
- Spec::Rake::SpecTask.new('specs_with_rcov') do |t|
117
- t.spec_files = FileList['test/spec/*_spec.rb']
77
+ desc "Run specs with coverage"
78
+ Spec::Rake::SpecTask.new('spec') do |t|
79
+ t.spec_files = FileList['spec/*_spec.rb']
118
80
  t.rcov = true
119
81
  end
120
82
 
121
- require 'spec/rake/rcov_verify'
83
+ #require 'spec/rake/rcov_verify'
122
84
 
123
- RCov::VerifyTask.new(:rcov_verify => :rcov) do |t|
124
- t.threshold = 95.4 # Make sure you have rcov 0.7 or higher!
125
- t.index_html = 'doc/output/coverage/index.html'
126
- end
85
+ #RCov::VerifyTask.new(:rcov_verify => :rcov) do |t|
86
+ # t.threshold = 95.4 # Make sure you have rcov 0.7 or higher!
87
+ # t.index_html = 'coverage/index.html'
88
+ #end
127
89
 
128
90
  ##############################################################################
129
91
  # Statistics
@@ -131,9 +93,7 @@ end
131
93
 
132
94
  STATS_DIRECTORIES = [
133
95
  %w(Code lib/),
134
- %w(Unit\ tests test/unit),
135
- %w(Functional\ tests test/functional),
136
- %w(Specification\ tests test/spec)
96
+ %w(Specification\ tests spec)
137
97
  ].collect { |name, dir| [ name, "./#{dir}" ] }.select { |name, dir| File.directory?(dir) }
138
98
 
139
99
  desc "Report code statistics (KLOCs, etc) from the application"
data/bin/serverside CHANGED
@@ -76,7 +76,7 @@ if $cmd == 'serve'
76
76
  puts "Serving at #{$cmd_config[:host]}:#{$cmd_config[:ports].begin}..."
77
77
  trap('INT') {exit}
78
78
  ServerSide::HTTP::Server.new($cmd_config[:host], $cmd_config[:ports].begin,
79
- ServerSide::Router)
79
+ ServerSide::Router).start
80
80
  else
81
81
  ServerSide::Application.daemonize($cmd_config, $cmd)
82
82
  end
@@ -15,7 +15,8 @@ module ServerSide
15
15
  daemon_class = Class.new(Daemon::Cluster) do
16
16
  meta_def(:pid_fn) {Daemon::WorkingDirectory/'serverside.pid'}
17
17
  meta_def(:server_loop) do |port|
18
- ServerSide::HTTP::Server.new(config[:host], port, ServerSide::Router)
18
+ ServerSide::HTTP::Server.new(
19
+ config[:host], port, ServerSide::Router).start
19
20
  end
20
21
  meta_def(:ports) {config[:ports]}
21
22
  end
@@ -4,21 +4,39 @@ module ServerSide
4
4
  module HTTP
5
5
  # This module implements HTTP cache negotiation with a client.
6
6
  module Caching
7
+ # HTTP headers
7
8
  ETAG = 'ETag'.freeze
9
+ LAST_MODIFIED = 'Last-Modified'.freeze
10
+ EXPIRES = 'Expires'.freeze
8
11
  CACHE_CONTROL = 'Cache-Control'.freeze
9
- DEFAULT_MAX_AGE = (86400 * 30).freeze
12
+ VARY = 'Vary'.freeze
13
+
10
14
  IF_NONE_MATCH = 'If-None-Match'.freeze
11
- ETAG_WILDCARD = '*'.freeze
12
15
  IF_MODIFIED_SINCE = 'If-Modified-Since'.freeze
13
- LAST_MODIFIED = "Last-Modified".freeze
14
- NOT_MODIFIED_CLOSE = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nConnection: close\r\nLast-Modified: %s\r\nETag: \"%s\"\r\nCache-Control: max-age=%d\r\n\r\n".freeze
15
- NOT_MODIFIED_PERSIST = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nLast-Modified: %s\r\nETag: \"%s\"\r\nCache-Control: max-age=%d\r\n\r\n".freeze
16
- MAX_AGE = 'max-age=%d'.freeze
16
+ WILDCARD = '*'.freeze
17
+
18
+ # Header values
19
+ NO_CACHE = 'no-cache'.freeze
17
20
  IF_NONE_MATCH_REGEXP = /^"?([^"]+)"?$/.freeze
18
-
19
- # Returns an array containing all etags specified by the client in the
20
- # If-None-Match header.
21
- def cache_etags
21
+
22
+ # etags
23
+ EXPIRY_ETAG_REGEXP = /(\d+)-(\d+)/.freeze
24
+ EXPIRY_ETAG_FORMAT = "%d-%d".freeze
25
+ ETAG_QUOTE_FORMAT = '"%s"'.freeze
26
+
27
+ # 304 formats
28
+ NOT_MODIFIED_CLOSE = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nConnection: close\r\nContent-Length: 0\r\n\r\n".freeze
29
+ NOT_MODIFIED_PERSIST = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nContent-Length: 0\r\n\r\n".freeze
30
+
31
+ def disable_caching
32
+ @response_headers[CACHE_CONTROL] = NO_CACHE
33
+ @response_headers.delete(ETAG)
34
+ @response_headers.delete(LAST_MODIFIED)
35
+ @response_headers.delete(EXPIRES)
36
+ @response_headers.delete(VARY)
37
+ end
38
+
39
+ def etag_validators
22
40
  h = @headers[IF_NONE_MATCH]
23
41
  return [] unless h
24
42
  h.split(',').inject([]) do |m, i|
@@ -26,54 +44,48 @@ module ServerSide
26
44
  end
27
45
  end
28
46
 
29
- # Returns the cache stamp specified by the client in the
30
- # If-Modified-Since header. If no stamp is specified, returns nil.
31
- def cache_stamp
32
- (h = @headers[IF_MODIFIED_SINCE]) ? Time.httpdate(h) : nil
33
- rescue
47
+ def valid_etag?(etag = nil)
48
+ if etag
49
+ etag_validators.each {|e| return true if e == etag || e == WILDCARD}
50
+ else
51
+ etag_validators.each do |e|
52
+ return true if e == WILDCARD ||
53
+ ((e =~ EXPIRY_ETAG_REGEXP) && (Time.at($2.to_i) > Time.now))
54
+ end
55
+ end
34
56
  nil
35
57
  end
36
-
37
- # Checks the request headers for validators and returns true if the
38
- # client cache is valid. The validators can be either etags (specified
39
- # in the If-None-Match header), or a modification stamp (specified in the
40
- # If-Modified-Since header.)
41
- def valid_client_cache?(etag, http_stamp)
42
- none_match = @headers[IF_NONE_MATCH]
43
- modified_since = @headers[IF_MODIFIED_SINCE]
44
- (none_match && (none_match =~ /\*|"#{etag}"/)) ||
45
- (modified_since && (modified_since == http_stamp))
58
+
59
+ def expiry_etag(stamp, max_age)
60
+ EXPIRY_ETAG_FORMAT % [stamp.to_i, (stamp + max_age).to_i]
61
+ end
62
+
63
+ def valid_stamp?(stamp)
64
+ return true if (modified_since = @headers[IF_MODIFIED_SINCE]) &&
65
+ (modified_since == stamp.httpdate)
46
66
  end
47
67
 
48
- # Validates the client cache by checking any supplied validators in the
49
- # request. If the client cache is not valid, the specified block is
50
- # executed. This method also makes sure the correct validators are
51
- # included in the response - along with a Cache-Control header, to allow
52
- # the client to cache the response. A possible usage:
53
- #
54
- # validate_cache("1234-5678", Time.now, 360) do
55
- # send_response(200, "text/html", body)
56
- # end
57
- def validate_cache(etag, stamp, max_age = DEFAULT_MAX_AGE, &block)
58
- http_stamp = stamp.httpdate
59
- if valid_client_cache?(etag, http_stamp)
60
- send_not_modified(etag, http_stamp, max_age)
68
+ def validate_cache(stamp, max_age, etag = nil,
69
+ cache_control = nil, vary = nil, &block)
70
+
71
+ if valid_etag?(etag) || valid_stamp?(stamp)
72
+ send_not_modified_response
73
+ true
61
74
  else
62
- @response_headers[ETAG] = "\"#{etag}\""
63
- @response_headers[LAST_MODIFIED] = http_stamp
64
- @response_headers[CACHE_CONTROL] = MAX_AGE % max_age
65
- block.call
75
+ @response_headers[ETAG] = ETAG_QUOTE_FORMAT %
76
+ [etag || expiry_etag(stamp, max_age)]
77
+ @response_headers[LAST_MODIFIED] = stamp.httpdate
78
+ @response_headers[EXPIRES] = (stamp + max_age).httpdate
79
+ @response_headers[CACHE_CONTROL] = cache_control if cache_control
80
+ @response_headers[VARY] = vary if vary
81
+ block ? block.call : nil
66
82
  end
67
83
  end
68
-
69
- # Sends a 304 HTTP response, along with etag and stamp validators, and a
70
- # Cache-Control header.
71
- def send_not_modified(etag, http_time, max_age = DEFAULT_MAX_AGE)
72
- @socket << ((@persistent ? NOT_MODIFIED_PERSIST : NOT_MODIFIED_CLOSE) %
73
- [Time.now.httpdate, http_time, etag, max_age])
84
+
85
+ def send_not_modified_response
86
+ @socket << ((@persistent ? NOT_MODIFIED_PERSIST : NOT_MODIFIED_CLOSE) %
87
+ Time.now.httpdate)
74
88
  end
75
89
  end
76
90
  end
77
91
  end
78
-
79
- __END__
@@ -0,0 +1,91 @@
1
+ require File.join(File.dirname(__FILE__), 'routing')
2
+ require 'rubygems'
3
+ require 'metaid'
4
+
5
+ module ServerSide
6
+ # Implements a basic controller class for handling requests. Controllers can
7
+ # be mounted by using the Controller.mount
8
+ class Controller
9
+ # Creates a subclass of Controller which adds a routing rule when
10
+ # subclassed. For example:
11
+ #
12
+ # class MyController < ServerSide::Controller.mount('/ohmy')
13
+ # def response
14
+ # render('Hi there!', 'text/plain')
15
+ # end
16
+ # end
17
+ #
18
+ # You can of course route according to any rule as specified in
19
+ # ServerSide::Router.route, including passing a block as a rule, e.g.:
20
+ #
21
+ # class MyController < ServerSide::Controller.mount {@headers['Accept'] =~ /wap/}
22
+ # ...
23
+ # end
24
+ def self.mount(rule = nil, &block)
25
+ rule ||= block
26
+ raise ArgumentError, "No routing rule specified." if rule.nil?
27
+ Class.new(self) do
28
+ meta_def(:inherited) do |sub_class|
29
+ ServerSide::Router.route(rule) {sub_class.new(self)}
30
+ end
31
+ end
32
+ end
33
+
34
+ # Initialize a new controller instance. Sets @request to the request object
35
+ # and copies both the request path and parameters to instance variables.
36
+ # After calling response, this method checks whether a response has been sent
37
+ # (rendered), and if not, invokes the render_default method.
38
+ def initialize(request)
39
+ @request = request
40
+ @path = request.path
41
+ @parameters = request.parameters
42
+ response
43
+ render_default if not @rendered
44
+ end
45
+
46
+ # Renders the response. This method should be overriden.
47
+ def response
48
+ end
49
+
50
+ # Sends a default response.
51
+ def render_default
52
+ @request.send_response(200, 'text/plain', 'no response.')
53
+ end
54
+
55
+ # Sends a response and sets @rendered to true.
56
+ def render(body, content_type)
57
+ @request.send_response(200, content_type, body)
58
+ @rendered = true
59
+ end
60
+ end
61
+ end
62
+
63
+ __END__
64
+
65
+ class ServerSide::ActionController < ServerSide::Controller
66
+ def self.default_routing_rule
67
+ if name.split('::').last =~ /(.+)Controller$/
68
+ controller = Inflector.underscore($1)
69
+ {:path => ["/#{controller}", "/#{controller}/:action", "/#{controller}/:action/:id"]}
70
+ end
71
+ end
72
+
73
+ def self.inherited(c)
74
+ routing_rule = c.respond_to?(:routing_rule) ?
75
+ c.routing_rule : c.default_routing_rule
76
+ if routing_rule
77
+ ServerSide::Router.route(routing_rule) {c.new(self)}
78
+ end
79
+ end
80
+
81
+ def self.route(arg = nil, &block)
82
+ rule = arg || block
83
+ meta_def(:get_route) {rule}
84
+ end
85
+ end
86
+
87
+ class MyController < ActionController
88
+ route "hello"
89
+ end
90
+
91
+ p MyController.get_route
@@ -15,6 +15,12 @@ class String
15
15
  def /(o)
16
16
  File.join(self, o.to_s)
17
17
  end
18
+
19
+ # Converts camel-cased phrases to underscored phrases.
20
+ def underscore
21
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').
22
+ tr("-", "_").downcase
23
+ end
18
24
  end
19
25
 
20
26
  # Symbol extensions and overrides.
@@ -1,5 +1,15 @@
1
1
  require 'fileutils'
2
2
 
3
+ class Object
4
+ def backtrace
5
+ raise RuntimeError
6
+ rescue => e
7
+ bt = e.backtrace.dup
8
+ bt.shift
9
+ bt
10
+ end
11
+ end
12
+
3
13
  # The Daemon module takes care of starting and stopping daemons.
4
14
  module Daemon
5
15
  WorkingDirectory = FileUtils.pwd
@@ -24,6 +34,10 @@ module Daemon
24
34
  rescue
25
35
  raise 'Pid not found. Is the daemon started?'
26
36
  end
37
+
38
+ def self.remove(daemon)
39
+ FileUtils.rm(daemon.pid_fn) if File.file?(daemon.pid_fn)
40
+ end
27
41
  end
28
42
 
29
43
  # Controls a daemon according to the supplied command or command-line
@@ -50,18 +64,24 @@ module Daemon
50
64
  PidFile.store(daemon, Process.pid)
51
65
  Dir.chdir WorkingDirectory
52
66
  File.umask 0000
53
- STDIN.reopen "/dev/null"
54
- STDOUT.reopen "/dev/null", "a"
55
- STDERR.reopen STDOUT
67
+ # STDIN.reopen "/dev/null"
68
+ # STDOUT.reopen "/dev/null", "a"
69
+ # STDERR.reopen STDOUT
56
70
  trap("TERM") {daemon.stop; exit}
57
71
  daemon.start
58
72
  end
59
73
  end
60
-
74
+
61
75
  # Stops the daemon by sending it a TERM signal.
62
76
  def self.stop(daemon)
63
77
  pid = PidFile.recall(daemon)
64
- FileUtils.rm(daemon.pid_fn)
65
78
  pid && Process.kill("TERM", pid)
79
+ PidFile.remove(daemon)
80
+ end
81
+
82
+ def self.alive?(daemon)
83
+ pid = PidFile.recall(daemon) rescue nil
84
+ return nil if !pid
85
+ `ps #{pid}` =~ /#{pid}/ ? true : nil
66
86
  end
67
87
  end
@@ -123,15 +123,16 @@ module ServerSide
123
123
  @parameters.merge! parse_parameters(@body)
124
124
  elsif @content_type =~ MULTIPART_REGEXP
125
125
  boundary = "--#$1"
126
- @body.split(/(?:\r?\n|\A)#{Regexp::quote(boundary)}(?:--)?\r\n/m).each do |pt|
126
+ r = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r\n/m
127
+ @body.split(r).each do |pt|
127
128
  headers, payload = pt.split("\r\n\r\n", 2)
128
129
  atts = {}
129
130
  if headers =~ CONTENT_DISPOSITION_REGEXP
130
- $1.split(';').map {|part|
131
+ $1.split(';').map do |part|
131
132
  if part =~ FIELD_ATTRIBUTE_REGEXP
132
133
  atts[$1.to_sym] = $2
133
134
  end
134
- }
135
+ end
135
136
  end
136
137
  if headers =~ CONTENT_TYPE_REGEXP
137
138
  atts[:type] = $1
@@ -150,16 +151,17 @@ module ServerSide
150
151
  @response_headers.merge!(headers) if headers
151
152
  h = @response_headers.inject('') {|m, kv| m << (HEADER % kv)}
152
153
 
153
- # calculate content_length if needed. if we dont have the content_length,
154
- # we consider the response as a streaming response, and so the connection
155
- # will not be persistent.
154
+ # calculate content_length if needed. if we dont have the
155
+ # content_length, we consider the response as a streaming response,
156
+ # and so the connection will not be persistent.
156
157
  content_length = body.length if content_length.nil? && body
157
158
  @persistent = false if content_length.nil?
158
159
 
159
160
  # Select the right format to use according to circumstances.
160
161
  @socket << ((@persistent ? STATUS_PERSIST :
161
162
  (body ? STATUS_CLOSE : STATUS_STREAM)) %
162
- [status, Time.now.httpdate, content_type, h, @response_cookies, content_length])
163
+ [status, Time.now.httpdate, content_type, h, @response_cookies,
164
+ content_length])
163
165
  @socket << body if body
164
166
  rescue
165
167
  @persistent = false
@@ -168,8 +170,10 @@ module ServerSide
168
170
  CONTENT_DISPOSITION = 'Content-Disposition'.freeze
169
171
  CONTENT_DESCRIPTION = 'Content-Description'.freeze
170
172
 
171
- def send_file(content, content_type, disposition = :inline, filename = nil, description = nil)
172
- disposition = filename ? "#{disposition}; filename=#{filename}" : "#{disposition}"
173
+ def send_file(content, content_type, disposition = :inline,
174
+ filename = nil, description = nil)
175
+ disposition = filename ?
176
+ "#{disposition}; filename=#{filename}" : disposition
173
177
  @response_headers[CONTENT_DISPOSITION] = disposition
174
178
  @response_headers[CONTENT_DESCRIPTION] = description if description
175
179
  send_response(200, content_type, content)
@@ -192,10 +196,12 @@ module ServerSide
192
196
  # Sets a cookie to be included in the response.
193
197
  def set_cookie(name, value, expires)
194
198
  @response_cookies ||= ""
195
- @response_cookies << (SET_COOKIE % [name, value.to_s.uri_escape, expires.rfc2822])
199
+ @response_cookies <<
200
+ (SET_COOKIE % [name, value.to_s.uri_escape, expires.rfc2822])
196
201
  end
197
202
 
198
- # Marks a cookie as deleted. The cookie is given an expires stamp in the past.
203
+ # Marks a cookie as deleted. The cookie is given an expires stamp in
204
+ # the past.
199
205
  def delete_cookie(name)
200
206
  set_cookie(name, nil, COOKIE_EXPIRED_TIME)
201
207
  end
@@ -22,9 +22,11 @@ module ServerSide
22
22
  # Routing rules are evaluated backwards, so the rules should be ordered
23
23
  # from the general to the specific.
24
24
  class Router < HTTP::Request
25
+ @@rules = []
26
+
25
27
  # Returns true if routes were defined.
26
- def self.has_routes?
27
- @@rules && !@@rules.empty? rescue false
28
+ def self.routes_defined?
29
+ !@@rules.empty? || @@default_route
28
30
  end
29
31
 
30
32
  # Adds a routing rule. The normalized rule is a hash containing keys (acting
@@ -36,7 +38,6 @@ module ServerSide
36
38
  #
37
39
  # ServerSide.route(lambda{path = 'mypage'}) {serve_static('mypage.html')}
38
40
  def self.route(rule, &block)
39
- @@rules ||= []
40
41
  rule = {:path => rule} unless (Hash === rule) || (Proc === rule)
41
42
  @@rules.unshift [rule, block]
42
43
  compile_rules
@@ -45,7 +46,6 @@ module ServerSide
45
46
  # Compiles all rules into a respond method that is invoked when a request
46
47
  # is received.
47
48
  def self.compile_rules
48
- @@rules ||= []
49
49
  code = @@rules.inject('lambda {') {|m, r| m << rule_to_statement(r[0], r[1])}
50
50
  code << 'default_handler}'
51
51
  define_method(:respond, &eval(code))
@@ -66,15 +66,15 @@ module ServerSide
66
66
  end
67
67
  }.join('&&')
68
68
  end
69
- "return #{proc_tag} if #{cond}\n"
69
+ "if #{cond} && (r = #{proc_tag}); return r; end\n"
70
70
  end
71
71
 
72
- # Pattern for finding parameters inside patterns. Parameters are parts of the
73
- # pattern, which the routing pre-processor turns into sub-regexp that are
74
- # used to extract parameter values from the pattern.
72
+ # Pattern for finding parameters inside patterns. Parameters are parts
73
+ # of the pattern, which the routing pre-processor turns into sub-regexp
74
+ # that are used to extract parameter values from the pattern.
75
75
  #
76
- # For example, matching '/controller/show' against '/controller/:action' will
77
- # give us @parameters[:action] #=> "show"
76
+ # For example, matching '/controller/show' against '/controller/:action'
77
+ # will give us @parameters[:action] #=> "show"
78
78
  ParamRegexp = /(?::([a-z]+))/
79
79
 
80
80
  # Returns the condition part for the key and value specified. The key is the
@@ -117,6 +117,7 @@ module ServerSide
117
117
 
118
118
  # Sets the default handler for incoming requests.
119
119
  def self.default_route(&block)
120
+ @@default_route = block
120
121
  define_method(:default_handler, &block)
121
122
  compile_rules
122
123
  end
@@ -6,13 +6,21 @@ module ServerSide
6
6
  # designed to support both HTTP 1.1 persistent connections, and HTTP streaming
7
7
  # for applications which use Comet techniques.
8
8
  class Server
9
- # Creates a new server by opening a listening socket and starting an accept
10
- # loop. When a new connection is accepted, a new instance of the
11
- # supplied connection class is instantiated and passed the connection for
12
- # processing.
9
+ attr_reader :listener
10
+
11
+ # Creates a new server by opening a listening socket.
13
12
  def initialize(host, port, request_class)
14
- @server = TCPServer.new(host, port)
15
- loop {Connection.new(@server.accept, request_class)}
13
+ @request_class = request_class
14
+ @listener = TCPServer.new(host, port)
15
+ end
16
+
17
+ # starts an accept loop. When a new connection is accepted, a new
18
+ # instance of the supplied connection class is instantiated and passed
19
+ # the connection for processing.
20
+ def start
21
+ while true
22
+ Connection.new(@listener.accept, @request_class)
23
+ end
16
24
  end
17
25
  end
18
26
  end