serverside 0.2.9 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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