m2r 1.0.0 → 2.0.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.
@@ -17,8 +17,12 @@ module M2R
17
17
 
18
18
  # @param [ConnectionFactory, Connection, #connection] connection_factory
19
19
  # Factory for generating connections
20
- def initialize(connection_factory)
20
+ #
21
+ # @param [#parse] parser
22
+ # Parser of M2 requests
23
+ def initialize(connection_factory, parser)
21
24
  @connection = connection_factory.connection
25
+ @parser = parser
22
26
  end
23
27
 
24
28
  # Start processing request
@@ -44,12 +48,14 @@ module M2R
44
48
  # Callback when a request is received
45
49
  # @api public
46
50
  # @!visibility public
51
+ # @param [Request] request Request object
47
52
  def on_request(request)
48
53
  end
49
54
 
50
55
  # Override to return a response
51
56
  # @api public
52
57
  # @!visibility public
58
+ # @param [Request] request Request object
53
59
  # @return [Response, String, #to_s] Response that should be sent to
54
60
  # Mongrel2 instance
55
61
  def process(request)
@@ -60,24 +66,30 @@ module M2R
60
66
  # because client already disconnected.
61
67
  # @api public
62
68
  # @!visibility public
69
+ # @param [Request] request Request object
63
70
  def on_disconnect(request)
64
71
  end
65
72
 
66
73
  # Callback when async-upload started
67
74
  # @api public
68
75
  # @!visibility public
76
+ # @param [Request] request Request object
69
77
  def on_upload_start(request)
70
78
  end
71
79
 
72
80
  # Callback when async-upload finished
73
81
  # @api public
74
82
  # @!visibility public
83
+ # @param [Request] request Request object
75
84
  def on_upload_done(request)
76
85
  end
77
86
 
78
87
  # Callback after process_request is done
79
88
  # @api public
80
89
  # @!visibility public
90
+ # @param [Request] request Request object
91
+ # @param [Response, String, #to_s] response Response that should be sent to
92
+ # Mongrel2 instance
81
93
  def after_process(request, response)
82
94
  return response
83
95
  end
@@ -85,6 +97,9 @@ module M2R
85
97
  # Callback after sending the response back
86
98
  # @api public
87
99
  # @!visibility public
100
+ # @param [Request] request Request object
101
+ # @param [Response, String, #to_s] response Response that was sent to
102
+ # Mongrel2 instance
88
103
  def after_reply(request, response)
89
104
  end
90
105
 
@@ -93,18 +108,39 @@ module M2R
93
108
  # resources (closing files etc)
94
109
  # @api public
95
110
  # @!visibility public
111
+ # @note `response` might be nil depending on when exception occured.
112
+ # @note In case of error this callback is called before on_error
113
+ # @param [Request] request Request object
114
+ # @param [Response, String, #to_s, nil] response Response that was sent to
115
+ # Mongrel2 instance
96
116
  def after_all(request, response)
97
117
  end
98
118
 
119
+ # Callback when exception occured
120
+ # @api public
121
+ # @!visibility public
122
+ # @note `request` and/or `response` might be nil depending on when error occured
123
+ # @param [Request, nil] request Request object
124
+ # @param [Response, String, #to_s, nil] response Response that might have been sent to
125
+ # Mongrel2 instance
126
+ # @param [StandardError] error
127
+ def on_error(request, response, error)
128
+ end
129
+
99
130
  private
100
131
 
132
+ def next_request
133
+ @parser.parse @connection.receive
134
+ end
135
+
101
136
  def one_loop
102
137
  on_wait
103
138
  throw :stop if stop?
104
- request_lifecycle(@connection.receive)
139
+ response = request_lifecycle(request = next_request)
140
+ rescue => error
141
+ on_error(request, response, error)
105
142
  end
106
143
 
107
-
108
144
  def request_lifecycle(request)
109
145
  on_request(request)
110
146
 
@@ -118,6 +154,7 @@ module M2R
118
154
  @connection.reply(request, response)
119
155
 
120
156
  after_reply(request, response)
157
+ return response
121
158
  ensure
122
159
  after_all(request, response)
123
160
  end
@@ -7,7 +7,11 @@ module M2R
7
7
  include Enumerable
8
8
 
9
9
  # @param [Hash, #inject] hash Collection of headers
10
- def initialize(hash = {})
10
+ # @param [true, false] compatible Whether the hash already contains
11
+ # downcased strings only. If so it is going to be directly as
12
+ # container for the headers.
13
+ def initialize(hash = {}, compatible = false)
14
+ @headers = hash and return if compatible
11
15
  @headers = hash.inject({}) do |headers,(header,value)|
12
16
  headers[transform_key(header)] = value
13
17
  headers
@@ -63,6 +67,10 @@ module M2R
63
67
  env
64
68
  end
65
69
 
70
+ def empty?
71
+ @headers.empty?
72
+ end
73
+
66
74
  protected
67
75
 
68
76
  def transform_key(key)
@@ -0,0 +1,30 @@
1
+ module M2R
2
+ module HTTP
3
+
4
+ # Detect that whether connection should be closed
5
+ # based on http protocol version and `Connection' header
6
+ # We do not support persistent connections for HTTP 1.0
7
+ module Close
8
+
9
+ # @return [true, false] Information whether HTTP Connection should
10
+ # be closed after processing the request. Happens when HTTP/1.0
11
+ # or request has Connection=close header.
12
+ def close?
13
+ unsupported_version? || connection_close?
14
+ end
15
+
16
+ protected
17
+
18
+ # http://en.wikipedia.org/wiki/HTTP_persistent_connection
19
+ def unsupported_version?
20
+ http_version != 'HTTP/1.1'
21
+ end
22
+
23
+ def connection_close?
24
+ headers['Connection'] == 'close'
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -7,9 +7,9 @@ module M2R
7
7
  class RackHandler < Handler
8
8
  attr_accessor :app
9
9
 
10
- def initialize(app, connection_factory)
10
+ def initialize(app, connection_factory, parser)
11
11
  @app = app
12
- super(connection_factory)
12
+ super(connection_factory, parser)
13
13
 
14
14
  trap('INT') { stop }
15
15
  end
@@ -36,7 +36,7 @@ module M2R
36
36
  status, headers, body = @app.call(env)
37
37
  buffer = ""
38
38
  body.each { |part| buffer << part }
39
- return Response.new(status, headers, buffer)
39
+ return Response.new.status(status).headers(headers).body(buffer)
40
40
  end
41
41
 
42
42
  def after_all(request, response)
@@ -0,0 +1,15 @@
1
+ require 'm2r/response'
2
+ require 'm2r/response/content_length'
3
+ require 'm2r/response/to_request'
4
+
5
+ module M2R
6
+ # Response object to be used without any other framework
7
+ # doing the job of handling content lenght and dealing with
8
+ # 'Connection' header.
9
+ #
10
+ # @api public
11
+ class Reply < Response
12
+ include Response::ContentLength
13
+ include Response::ToRequest
14
+ end
15
+ end
@@ -1,4 +1,6 @@
1
+ require 'set'
1
2
  require 'm2r'
3
+ require 'm2r/http/close'
2
4
  require 'm2r/request/base'
3
5
  require 'm2r/request/upload'
4
6
  require 'm2r/headers'
@@ -8,10 +10,17 @@ module M2R
8
10
  # @api public
9
11
  class Request
10
12
  # @api private
11
- TRUE_STRINGS = %w(true yes on 1).map(&:freeze).freeze
13
+ TRUE_STRINGS = Set.new(%w(true yes on 1).map(&:freeze)).freeze
14
+ # @api private
15
+ MONGREL2_BASE_HEADERS = Set.new(%w(pattern method path query url_scheme version).map(&:upcase).map(&:freeze)).freeze
16
+ # @api private
17
+ MONGREL2_UPLOAD_HEADERS = Set.new(%w(x-mongrel2-upload-start x-mongrel2-upload-done).map(&:downcase).map(&:freeze)).freeze
18
+ # @api private
19
+ MONGREL2_HEADERS = (MONGREL2_BASE_HEADERS + MONGREL2_UPLOAD_HEADERS).freeze
12
20
 
13
21
  include Base
14
22
  include Upload
23
+ include HTTP::Close
15
24
 
16
25
  # @return [String] UUID of mongrel2 origin instance
17
26
  attr_reader :sender
@@ -29,14 +38,16 @@ module M2R
29
38
  # @param [String] conn_id Mongrel2 connection id sending this request
30
39
  # @param [String] path HTTP Path of request
31
40
  # @param [M2R::Headers] headers HTTP headers of request
41
+ # @param [M2R::Headers] headers Additional mongrel2 headers
32
42
  # @param [String] body HTTP Body of request
33
- def initialize(sender, conn_id, path, headers, body)
34
- @http_headers, @mongrel_headers = split_headers(headers)
35
- @sender = sender
36
- @conn_id = conn_id
37
- @path = path
38
- @body = body
39
- @data = MultiJson.load(@body) if json?
43
+ def initialize(sender, conn_id, path, http_headers, mongrel_headers, body)
44
+ @sender = sender
45
+ @conn_id = conn_id
46
+ @path = path
47
+ @http_headers = http_headers
48
+ @mongrel_headers = mongrel_headers
49
+ @body = body
50
+ @data = MultiJson.load(@body) if json?
40
51
  end
41
52
 
42
53
  # Parse Mongrel2 request received via ZMQ message
@@ -51,8 +62,11 @@ module M2R
51
62
 
52
63
  headers, rest = TNetstring.parse(rest)
53
64
  body, _ = TNetstring.parse(rest)
54
- headers = Headers.new MultiJson.load(headers)
55
- self.new(sender, conn_id, path, headers, body)
65
+ headers = MultiJson.load(headers)
66
+ headers, mong = split_headers(headers)
67
+ headers = Headers.new headers, true
68
+ mong = Headers.new mong, true
69
+ self.new(sender, conn_id, path, headers, mong, body)
56
70
  end
57
71
 
58
72
  # @return [M2R::Headers] HTTP headers
@@ -80,6 +94,10 @@ module M2R
80
94
  @mongrel_headers['url_scheme'] || mongrel17_scheme
81
95
  end
82
96
 
97
+ def http_version
98
+ @mongrel_headers['version']
99
+ end
100
+
83
101
  # @return [true, false] Internal mongrel2 message to handler issued when
84
102
  # message delivery is not possible because the client already
85
103
  # disconnected and there is no connection with such {#conn_id}
@@ -87,13 +105,6 @@ module M2R
87
105
  json? and @data['type'] == 'disconnect'
88
106
  end
89
107
 
90
- # @return [true, false] Information whether HTTP Connection should
91
- # be closed after processing the request. Happens when HTTP/1.0
92
- # or request has Connection=close header.
93
- def close?
94
- unsupported_version? or connection_close?
95
- end
96
-
97
108
  protected
98
109
 
99
110
  def mongrel17_scheme
@@ -105,25 +116,21 @@ module M2R
105
116
  (ENV['HTTPS'] || "").downcase
106
117
  end
107
118
 
108
- def unsupported_version?
109
- @http_headers['version'] != 'HTTP/1.1'
110
- end
111
-
112
- def connection_close?
113
- @http_headers['connection'] == 'close'
114
- end
115
-
116
119
  def json?
117
120
  method == 'JSON'
118
121
  end
119
122
 
120
- def split_headers(headers)
121
- mongrel = Headers.new
122
- mongrel_headers.each do |header|
123
- next unless headers[header]
124
- mongrel[header] = headers.delete(header)
123
+ def self.split_headers(headers)
124
+ http = {}
125
+ mongrel = {}
126
+ headers.each do |header, value|
127
+ if MONGREL2_HEADERS.include?(header)
128
+ mongrel[header.downcase] = value
129
+ else
130
+ http[header] = value
131
+ end
125
132
  end
126
- return headers, mongrel
133
+ return http, mongrel
127
134
  end
128
135
  end
129
136
  end
@@ -4,8 +4,6 @@ module M2R
4
4
  #
5
5
  # @private
6
6
  module Base
7
- MONGREL2_BASE_HEADERS = %w(pattern method path query url_scheme).map(&:freeze).freeze
8
-
9
7
  # @return [StringIO] Request body encapsulated in IO compatible object
10
8
  # @api public
11
9
  def body_io
@@ -22,12 +20,6 @@ module M2R
22
20
  body_io.close
23
21
  end
24
22
 
25
- protected
26
-
27
- def mongrel_headers
28
- MONGREL2_BASE_HEADERS
29
- end
30
-
31
23
  end
32
24
  end
33
25
 
@@ -3,10 +3,6 @@ module M2R
3
3
  # Contains methods for recognizing such requests and reading them.
4
4
  # @private
5
5
  module Upload
6
- # Headers related to async-upload feature
7
- # @private
8
- MONGREL2_UPLOAD_HEADERS = %w(x-mongrel2-upload-start x-mongrel2-upload-done).map(&:freeze).freeze
9
-
10
6
  # @return [true,false] True if this is async-upload related request
11
7
  # @api public
12
8
  def upload?
@@ -49,12 +45,6 @@ module M2R
49
45
  super
50
46
  File.delete(body_io.path) if upload_done?
51
47
  end
52
-
53
- protected
54
-
55
- def mongrel_headers
56
- super + MONGREL2_UPLOAD_HEADERS
57
- end
58
48
  end
59
49
  end
60
50
 
@@ -1,11 +1,14 @@
1
1
  require 'm2r'
2
+ require 'm2r/http/close'
2
3
  require 'm2r/response/content_length'
4
+ require 'm2r/response/to_request'
3
5
 
4
6
  module M2R
5
7
  # Simplest possible abstraction layer over HTTP request
6
8
  #
7
9
  # @api public
8
10
  class Response
11
+ include HTTP::Close
9
12
 
10
13
  # @private
11
14
  VERSION = "HTTP/1.1".freeze
@@ -66,37 +69,80 @@ module M2R
66
69
  }
67
70
  STATUS_CODES.freeze
68
71
 
69
- # @return [Fixnum] HTTP Status code
70
- attr_reader :status
72
+ attr_reader :reason
73
+
74
+ def initialize
75
+ status(200)
76
+ headers(Headers.new)
77
+ body("")
78
+ http_version(VERSION)
79
+ end
71
80
 
72
- # @return [Hash, Headers] Collection of response HTTP Headers
73
- attr_reader :headers
81
+ # @param [Fixnum, #to_i] value HTTP status code
82
+ def status(value = GETTER)
83
+ if value == GETTER
84
+ @status
85
+ else
86
+ @status = value.to_i
87
+ @reason = STATUS_CODES[@status]
88
+ self
89
+ end
90
+ end
74
91
 
75
- # @return [String] HTTP Body
76
- attr_reader :body
92
+ # @param [Hash] value HTTP headers
93
+ def headers(value = GETTER)
94
+ if value == GETTER
95
+ @headers
96
+ else
97
+ @headers = value
98
+ self
99
+ end
100
+ end
77
101
 
78
- # @return [String] HTTP Status code description
79
- attr_reader :reason
102
+ # @param [Hash] header HTTP header key
103
+ # @param [Hash] value HTTP header value
104
+ def header(header, value = GETTER)
105
+ if value == GETTER
106
+ @headers[header]
107
+ else
108
+ @headers[header] = value
109
+ self
110
+ end
111
+ end
112
+
113
+ # @param [String, nil] value HTTP body
114
+ def body(value = GETTER)
115
+ if value == GETTER
116
+ @body
117
+ else
118
+ @body = value
119
+ self
120
+ end
121
+ end
80
122
 
81
- # @param [Fixnum, #to_i] status HTTP status code
82
- # @param [Hash] headers HTTP headers
83
- # @param [String, nil] body HTTP body
84
- def initialize(status, headers, body = nil)
85
- @status = status.to_i
86
- @headers = headers
87
- @body = body || ""
88
- @reason = STATUS_CODES[status.to_i]
123
+ # @param [String, nil] version HTTP body
124
+ def http_version(value = GETTER)
125
+ if value == GETTER
126
+ @version
127
+ else
128
+ @version = value
129
+ self
130
+ end
89
131
  end
90
132
 
91
133
  # @return [String] HTTP Response
92
134
  def to_s
93
- response = "#{VERSION} #{status} #{reason}#{CRLF}"
135
+ response = "#{http_version} #{status} #{reason}#{CRLF}"
94
136
  unless headers.empty?
95
137
  response << headers.map { |h, v| "#{h}: #{v}" }.join(CRLF) << CRLF
96
138
  end
97
139
  response << CRLF
98
- response << body
140
+ response << body.to_s
99
141
  response
100
142
  end
143
+
144
+ protected
145
+
146
+ GETTER = Object.new
101
147
  end
102
148
  end