syntropy 0.29.0 → 0.30.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +2 -2
- data/CHANGELOG.md +5 -0
- data/README.md +0 -2
- data/lib/syntropy/app.rb +8 -8
- data/lib/syntropy/errors.rb +13 -12
- data/lib/syntropy/http/connection.rb +396 -0
- data/lib/syntropy/http/server.rb +174 -0
- data/lib/syntropy/http/status.rb +76 -0
- data/lib/syntropy/http.rb +5 -0
- data/lib/syntropy/json_api.rb +2 -5
- data/lib/syntropy/mime_types.rb +37 -0
- data/lib/syntropy/request/mock_adapter.rb +58 -0
- data/lib/syntropy/request/request_info.rb +236 -0
- data/lib/syntropy/request/response.rb +206 -0
- data/lib/syntropy/{request_extensions.rb → request/validation.rb} +4 -173
- data/lib/syntropy/request.rb +99 -0
- data/lib/syntropy/utils.rb +1 -1
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +3 -6
- data/syntropy.gemspec +2 -3
- data/test/app/about/_error.rb +1 -1
- data/test/app/api+.rb +1 -1
- data/test/app_custom/_site.rb +1 -1
- data/test/bm_router_proc.rb +3 -3
- data/test/helper.rb +6 -5
- data/test/test_app.rb +30 -30
- data/test/test_caching.rb +2 -2
- data/test/test_connection.rb +4 -4
- data/test/test_errors.rb +6 -6
- data/test/test_json_api.rb +10 -8
- data/test/test_mock_adapter.rb +59 -0
- data/test/test_request_info.rb +90 -0
- data/test/test_response.rb +112 -0
- data/test/test_server.rb +1 -1
- metadata +27 -17
- data/lib/syntropy/connection.rb +0 -402
- data/lib/syntropy/server.rb +0 -173
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'digest/sha1'
|
|
5
|
+
|
|
6
|
+
require_relative '../http/status'
|
|
7
|
+
require_relative '../mime_types'
|
|
8
|
+
|
|
9
|
+
module Syntropy
|
|
10
|
+
module StaticFileCaching
|
|
11
|
+
class << self
|
|
12
|
+
def file_stat_to_etag(stat)
|
|
13
|
+
"#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def file_stat_to_last_modified(stat)
|
|
17
|
+
stat.mtime.httpdate
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ResponseMethods
|
|
23
|
+
WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
24
|
+
|
|
25
|
+
def upgrade_to_websocket(custom_headers = nil)
|
|
26
|
+
key = "#{headers['sec-websocket-key']}#{WEBSOCKET_GUID}"
|
|
27
|
+
upgrade_headers = {
|
|
28
|
+
'Sec-WebSocket-Accept' => Digest::SHA1.base64digest(key)
|
|
29
|
+
}
|
|
30
|
+
upgrade_headers.merge!(custom_headers) if custom_headers
|
|
31
|
+
upgrade('websocket', upgrade_headers)
|
|
32
|
+
|
|
33
|
+
adapter.websocket_connection(self)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def redirect(url, status = HTTP::FOUND)
|
|
37
|
+
respond(nil, ':status' => status, 'Location' => url)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def redirect_to_https(status = HTTP::MOVED_PERMANENTLY)
|
|
41
|
+
secure_uri = "https://#{host}#{uri}"
|
|
42
|
+
redirect(secure_uri, status)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def redirect_to_host(new_host, status = HTTP::FOUND)
|
|
46
|
+
secure_uri = "//#{new_host}#{uri}"
|
|
47
|
+
redirect(secure_uri, status)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serve_file(path, opts = {})
|
|
51
|
+
full_path = file_full_path(path, opts)
|
|
52
|
+
stat = File.stat(full_path)
|
|
53
|
+
etag = StaticFileCaching.file_stat_to_etag(stat)
|
|
54
|
+
last_modified = StaticFileCaching.file_stat_to_last_modified(stat)
|
|
55
|
+
|
|
56
|
+
if validate_static_file_cache(etag, last_modified)
|
|
57
|
+
return respond(nil, {
|
|
58
|
+
':status' => HTTP::NOT_MODIFIED,
|
|
59
|
+
'etag' => etag
|
|
60
|
+
})
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
mime_type = MimeTypes[File.extname(path)]
|
|
64
|
+
opts[:stat] = stat
|
|
65
|
+
(opts[:headers] ||= {})['Content-Type'] ||= mime_type if mime_type
|
|
66
|
+
|
|
67
|
+
respond_with_static_file(full_path, etag, last_modified, opts)
|
|
68
|
+
rescue Errno::ENOENT
|
|
69
|
+
respond(nil, ':status' => HTTP::NOT_FOUND)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_static_file_cache(etag, last_modified)
|
|
73
|
+
if (none_match = headers['if-none-match'])
|
|
74
|
+
return true if none_match == etag
|
|
75
|
+
end
|
|
76
|
+
if (modified_since = headers['if-modified-since'])
|
|
77
|
+
return true if modified_since == last_modified
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def file_full_path(path, opts)
|
|
84
|
+
if (base_path = opts[:base_path])
|
|
85
|
+
File.join(opts[:base_path], path)
|
|
86
|
+
else
|
|
87
|
+
path
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def serve_io(io, opts)
|
|
92
|
+
respond(io.read, opts[:headers] || {})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def respond_with_static_file(path, etag, last_modified, opts)
|
|
96
|
+
cache_headers = (etag || last_modified) ? {
|
|
97
|
+
'etag' => etag,
|
|
98
|
+
'last-modified' => last_modified
|
|
99
|
+
} : {}
|
|
100
|
+
|
|
101
|
+
adapter.respond_with_static_file(self, path, opts, cache_headers)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def set_response_headers(headers)
|
|
105
|
+
adapter.set_response_headers(headers)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def set_cookie(*)
|
|
109
|
+
adapter.set_cookie(*)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def upgrade(protocol, custom_headers = nil, &block)
|
|
113
|
+
upgrade_headers = {
|
|
114
|
+
':status' => HTTP::SWITCHING_PROTOCOLS,
|
|
115
|
+
'Upgrade' => protocol,
|
|
116
|
+
'Connection' => 'upgrade'
|
|
117
|
+
}
|
|
118
|
+
upgrade_headers.merge!(custom_headers) if custom_headers
|
|
119
|
+
|
|
120
|
+
respond(nil, upgrade_headers)
|
|
121
|
+
adapter.with_stream(&block)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Responds according to the given map. The given map defines the responses
|
|
125
|
+
# for each method. The value for each method is either an array containing
|
|
126
|
+
# the body and header values to use as response, or a proc returning such an
|
|
127
|
+
# array. For example:
|
|
128
|
+
#
|
|
129
|
+
# req.respond_by_http_method(
|
|
130
|
+
# 'head' => [nil, headers],
|
|
131
|
+
# 'get' => -> { [IO.read(fn), headers] }
|
|
132
|
+
# )
|
|
133
|
+
#
|
|
134
|
+
# If the request's method is not included in the given map, an exception is
|
|
135
|
+
# raised.
|
|
136
|
+
#
|
|
137
|
+
# @param map [Hash] hash mapping HTTP methods to responses
|
|
138
|
+
# @return [void]
|
|
139
|
+
def respond_by_http_method(map)
|
|
140
|
+
value = map[self.method]
|
|
141
|
+
raise Syntropy::Error.method_not_allowed if !value
|
|
142
|
+
|
|
143
|
+
value = value.() if value.is_a?(Proc)
|
|
144
|
+
(body, headers) = value
|
|
145
|
+
respond(body, headers)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Responds to GET requests with the given body and headers. Otherwise raises
|
|
149
|
+
# an exception.
|
|
150
|
+
#
|
|
151
|
+
# @param body [String, nil] response body
|
|
152
|
+
# @param headers [Hash] response headers
|
|
153
|
+
# @return [void]
|
|
154
|
+
def respond_on_get(body, headers = {})
|
|
155
|
+
case self.method
|
|
156
|
+
when 'head'
|
|
157
|
+
respond(nil, headers)
|
|
158
|
+
when 'get'
|
|
159
|
+
respond(body, headers)
|
|
160
|
+
else
|
|
161
|
+
raise Syntropy::Error.method_not_allowed
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Responds to POST requests with the given body and headers. Otherwise
|
|
166
|
+
# raises an exception.
|
|
167
|
+
#
|
|
168
|
+
# @param body [String, nil] response body
|
|
169
|
+
# @param headers [Hash] response headers
|
|
170
|
+
# @return [void]
|
|
171
|
+
def respond_on_post(body, headers = {})
|
|
172
|
+
case self.method
|
|
173
|
+
when 'head'
|
|
174
|
+
respond(nil, headers)
|
|
175
|
+
when 'post'
|
|
176
|
+
respond(body, headers)
|
|
177
|
+
else
|
|
178
|
+
raise Syntropy::Error.method_not_allowed
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def html_response(html, **headers)
|
|
183
|
+
respond(
|
|
184
|
+
html,
|
|
185
|
+
'Content-Type' => 'text/html; charset=utf-8',
|
|
186
|
+
**headers
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def json_response(obj, **headers)
|
|
191
|
+
respond(
|
|
192
|
+
JSON.dump(obj),
|
|
193
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
|
194
|
+
**headers
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def json_pretty_response(obj, **headers)
|
|
199
|
+
respond(
|
|
200
|
+
JSON.pretty_generate(obj),
|
|
201
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
|
202
|
+
**headers
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -1,60 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
4
|
-
require '
|
|
5
|
-
|
|
6
|
-
class Qeweney::Request
|
|
7
|
-
attr_accessor :start_stamp
|
|
8
|
-
|
|
9
|
-
def respond_with_static_file(path, etag, last_modified, opts)
|
|
10
|
-
cache_headers = (etag || last_modified) ? {
|
|
11
|
-
'etag' => etag,
|
|
12
|
-
'last-modified' => last_modified
|
|
13
|
-
} : {}
|
|
14
|
-
|
|
15
|
-
adapter.respond_with_static_file(self, path, opts, cache_headers)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def set_response_headers(headers)
|
|
19
|
-
adapter.set_response_headers(headers)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def set_cookie(*)
|
|
23
|
-
adapter.set_cookie(*)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def upgrade(protocol, custom_headers = nil, &block)
|
|
27
|
-
super(protocol, custom_headers)
|
|
28
|
-
adapter.with_stream(&block)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'escape_utils'
|
|
31
5
|
|
|
32
6
|
module Syntropy
|
|
33
|
-
|
|
34
|
-
module RequestExtensions
|
|
35
|
-
attr_reader :route_params
|
|
36
|
-
attr_accessor :route
|
|
37
|
-
|
|
38
|
-
# Initializes request with additional fields
|
|
39
|
-
def initialize(headers, adapter)
|
|
40
|
-
@headers = headers
|
|
41
|
-
@adapter = adapter
|
|
42
|
-
@route = nil
|
|
43
|
-
@route_params = {}
|
|
44
|
-
@ctx = nil
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Sets up mock request additional fields
|
|
48
|
-
def setup_mock_request
|
|
49
|
-
@route = nil
|
|
50
|
-
@route_params = {}
|
|
51
|
-
@ctx = nil
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Returns the request context
|
|
55
|
-
def ctx
|
|
56
|
-
@ctx ||= {}
|
|
57
|
-
end
|
|
7
|
+
module RequestValidationMethods
|
|
58
8
|
|
|
59
9
|
# Checks the request's HTTP method against the given accepted values. If not
|
|
60
10
|
# included in the accepted values, raises an exception. Otherwise, returns
|
|
@@ -68,64 +18,6 @@ module Syntropy
|
|
|
68
18
|
raise Syntropy::Error.method_not_allowed
|
|
69
19
|
end
|
|
70
20
|
|
|
71
|
-
# Responds according to the given map. The given map defines the responses
|
|
72
|
-
# for each method. The value for each method is either an array containing
|
|
73
|
-
# the body and header values to use as response, or a proc returning such an
|
|
74
|
-
# array. For example:
|
|
75
|
-
#
|
|
76
|
-
# req.respond_by_http_method(
|
|
77
|
-
# 'head' => [nil, headers],
|
|
78
|
-
# 'get' => -> { [IO.read(fn), headers] }
|
|
79
|
-
# )
|
|
80
|
-
#
|
|
81
|
-
# If the request's method is not included in the given map, an exception is
|
|
82
|
-
# raised.
|
|
83
|
-
#
|
|
84
|
-
# @param map [Hash] hash mapping HTTP methods to responses
|
|
85
|
-
# @return [void]
|
|
86
|
-
def respond_by_http_method(map)
|
|
87
|
-
value = map[self.method]
|
|
88
|
-
raise Syntropy::Error.method_not_allowed if !value
|
|
89
|
-
|
|
90
|
-
value = value.() if value.is_a?(Proc)
|
|
91
|
-
(body, headers) = value
|
|
92
|
-
respond(body, headers)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Responds to GET requests with the given body and headers. Otherwise raises
|
|
96
|
-
# an exception.
|
|
97
|
-
#
|
|
98
|
-
# @param body [String, nil] response body
|
|
99
|
-
# @param headers [Hash] response headers
|
|
100
|
-
# @return [void]
|
|
101
|
-
def respond_on_get(body, headers = {})
|
|
102
|
-
case self.method
|
|
103
|
-
when 'head'
|
|
104
|
-
respond(nil, headers)
|
|
105
|
-
when 'get'
|
|
106
|
-
respond(body, headers)
|
|
107
|
-
else
|
|
108
|
-
raise Syntropy::Error.method_not_allowed
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Responds to POST requests with the given body and headers. Otherwise
|
|
113
|
-
# raises an exception.
|
|
114
|
-
#
|
|
115
|
-
# @param body [String, nil] response body
|
|
116
|
-
# @param headers [Hash] response headers
|
|
117
|
-
# @return [void]
|
|
118
|
-
def respond_on_post(body, headers = {})
|
|
119
|
-
case self.method
|
|
120
|
-
when 'head'
|
|
121
|
-
respond(nil, headers)
|
|
122
|
-
when 'post'
|
|
123
|
-
respond(body, headers)
|
|
124
|
-
else
|
|
125
|
-
raise Syntropy::Error.method_not_allowed
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
21
|
# Validates and optionally converts request parameter value for the given
|
|
130
22
|
# parameter name against the given clauses. If no clauses are given,
|
|
131
23
|
# verifies the parameter value is not nil. A clause can be a class, such as
|
|
@@ -185,7 +77,7 @@ module Syntropy
|
|
|
185
77
|
validated = true if client_mtime == last_modified
|
|
186
78
|
end
|
|
187
79
|
if validated
|
|
188
|
-
respond(nil, ':status' =>
|
|
80
|
+
respond(nil, ':status' => HTTP::NOT_MODIFIED)
|
|
189
81
|
else
|
|
190
82
|
cache_headers = {
|
|
191
83
|
'Cache-Control' => cache_control
|
|
@@ -197,67 +89,8 @@ module Syntropy
|
|
|
197
89
|
end
|
|
198
90
|
end
|
|
199
91
|
|
|
200
|
-
# Reads the request body and returns form data.
|
|
201
|
-
#
|
|
202
|
-
# @return [Hash] form data
|
|
203
|
-
def get_form_data
|
|
204
|
-
body = read
|
|
205
|
-
if !body || body.empty?
|
|
206
|
-
raise Syntropy::Error.new('Missing form data', Qeweney::Status::BAD_REQUEST)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
Qeweney::Request.parse_form_data(body, headers)
|
|
210
|
-
rescue Qeweney::BadRequestError
|
|
211
|
-
raise Syntropy::Error.new('Invalid form data', Qeweney::Status::BAD_REQUEST)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def html_response(html, **headers)
|
|
215
|
-
respond(
|
|
216
|
-
html,
|
|
217
|
-
'Content-Type' => 'text/html; charset=utf-8',
|
|
218
|
-
**headers
|
|
219
|
-
)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def json_response(obj, **headers)
|
|
223
|
-
respond(
|
|
224
|
-
JSON.dump(obj),
|
|
225
|
-
'Content-Type' => 'application/json; charset=utf-8',
|
|
226
|
-
**headers
|
|
227
|
-
)
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def json_pretty_response(obj, **headers)
|
|
231
|
-
respond(
|
|
232
|
-
JSON.pretty_generate(obj),
|
|
233
|
-
'Content-Type' => 'application/json; charset=utf-8',
|
|
234
|
-
**headers
|
|
235
|
-
)
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def browser?
|
|
239
|
-
user_agent = headers['user-agent']
|
|
240
|
-
user_agent && user_agent =~ /^Mozilla\//
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Returns true if the accept header includes the given MIME type
|
|
244
|
-
#
|
|
245
|
-
# @param mime_type [String] MIME type
|
|
246
|
-
# @return [bool]
|
|
247
|
-
def accept?(mime_type)
|
|
248
|
-
accept = headers['accept']
|
|
249
|
-
return nil if !accept
|
|
250
|
-
|
|
251
|
-
@accept_parts ||= parse_accept_parts(accept)
|
|
252
|
-
@accept_parts.include?(mime_type)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
92
|
private
|
|
256
93
|
|
|
257
|
-
def parse_accept_parts(accept)
|
|
258
|
-
accept.split(',').map { it.match(/^\s*([^\s;]+)/)[1] }
|
|
259
|
-
end
|
|
260
|
-
|
|
261
94
|
BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
|
|
262
95
|
BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
|
|
263
96
|
INTEGER_REGEXP = /^[+-]?[0-9]+$/
|
|
@@ -304,5 +137,3 @@ module Syntropy
|
|
|
304
137
|
end
|
|
305
138
|
end
|
|
306
139
|
end
|
|
307
|
-
|
|
308
|
-
Qeweney::Request.include(Syntropy::RequestExtensions)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './request/request_info'
|
|
4
|
+
require_relative './request/validation'
|
|
5
|
+
require_relative './request/response'
|
|
6
|
+
require_relative './http/status'
|
|
7
|
+
|
|
8
|
+
module Syntropy
|
|
9
|
+
class Request
|
|
10
|
+
include RequestInfoMethods
|
|
11
|
+
include RequestValidationMethods
|
|
12
|
+
include ResponseMethods
|
|
13
|
+
|
|
14
|
+
extend RequestInfoClassMethods
|
|
15
|
+
|
|
16
|
+
attr_reader :headers, :adapter, :start_stamp, :route_params
|
|
17
|
+
attr_accessor :route
|
|
18
|
+
|
|
19
|
+
def initialize(headers, adapter)
|
|
20
|
+
@headers = headers
|
|
21
|
+
@adapter = adapter
|
|
22
|
+
@start_stamp = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
23
|
+
@route = nil
|
|
24
|
+
@route_params = {}
|
|
25
|
+
@ctx = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the request context
|
|
29
|
+
def ctx
|
|
30
|
+
@ctx ||= {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def next_chunk
|
|
34
|
+
@adapter.get_body_chunk(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def each_chunk
|
|
38
|
+
while (chunk = @adapter.get_body_chunk(self))
|
|
39
|
+
yield chunk
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def read
|
|
44
|
+
@adapter.get_body(self)
|
|
45
|
+
end
|
|
46
|
+
alias_method :body, :read
|
|
47
|
+
|
|
48
|
+
def complete?
|
|
49
|
+
@adapter.complete?(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
EMPTY_HEADERS = {}.freeze
|
|
53
|
+
|
|
54
|
+
def respond(body, headers = EMPTY_HEADERS)
|
|
55
|
+
@adapter.respond(self, body, headers)
|
|
56
|
+
@headers_sent = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def send_headers(headers = EMPTY_HEADERS, empty_response = false)
|
|
60
|
+
return if @headers_sent
|
|
61
|
+
|
|
62
|
+
@headers_sent = true
|
|
63
|
+
@adapter.send_headers(self, headers, empty_response: empty_response)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def send_chunk(body, done: false)
|
|
67
|
+
send_headers({}) unless @headers_sent
|
|
68
|
+
|
|
69
|
+
@adapter.send_chunk(self, body, done: done)
|
|
70
|
+
end
|
|
71
|
+
alias_method :<<, :send_chunk
|
|
72
|
+
|
|
73
|
+
def finish
|
|
74
|
+
send_headers({}) unless @headers_sent
|
|
75
|
+
|
|
76
|
+
@adapter.finish(self)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def headers_sent?
|
|
80
|
+
@headers_sent
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rx_incr(count)
|
|
84
|
+
headers[':rx'] ? headers[':rx'] += count : headers[':rx'] = count
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def tx_incr(count)
|
|
88
|
+
headers[':tx'] ? headers[':tx'] += count : headers[':tx'] = count
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def transfer_counts
|
|
92
|
+
[headers[':rx'], headers[':tx']]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def total_transfer
|
|
96
|
+
(headers[':rx'] || 0) + (headers[':tx'] || 0)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/syntropy/utils.rb
CHANGED
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'qeweney'
|
|
4
3
|
require 'uringmachine'
|
|
5
4
|
require 'papercraft'
|
|
6
5
|
|
|
6
|
+
require 'syntropy/request'
|
|
7
7
|
require 'syntropy/logger'
|
|
8
|
-
require 'syntropy/
|
|
9
|
-
require 'syntropy/
|
|
8
|
+
require 'syntropy/http'
|
|
9
|
+
require 'syntropy/mime_types'
|
|
10
10
|
require 'syntropy/app'
|
|
11
11
|
require 'syntropy/connection_pool'
|
|
12
12
|
require 'syntropy/errors'
|
|
13
13
|
require 'syntropy/markdown'
|
|
14
14
|
require 'syntropy/module'
|
|
15
|
-
require 'syntropy/request_extensions'
|
|
16
15
|
require 'syntropy/papercraft_extensions'
|
|
17
16
|
require 'syntropy/routing_tree'
|
|
18
17
|
require 'syntropy/json_api'
|
|
@@ -21,8 +20,6 @@ require 'syntropy/utils'
|
|
|
21
20
|
require 'syntropy/version'
|
|
22
21
|
|
|
23
22
|
module Syntropy
|
|
24
|
-
Status = Qeweney::Status
|
|
25
|
-
|
|
26
23
|
extend Utilities
|
|
27
24
|
|
|
28
25
|
class << self
|
data/syntropy.gemspec
CHANGED
|
@@ -23,10 +23,9 @@ Gem::Specification.new do |s|
|
|
|
23
23
|
|
|
24
24
|
s.add_dependency 'extralite', '~>2.14'
|
|
25
25
|
s.add_dependency 'papercraft', '~>3.2.0'
|
|
26
|
-
s.add_dependency 'qeweney', '~>0.24'
|
|
27
26
|
s.add_dependency 'uringmachine', '~>1.0.0'
|
|
28
|
-
|
|
29
|
-
s.add_dependency '
|
|
27
|
+
s.add_dependency 'cgi'
|
|
28
|
+
s.add_dependency 'escape_utils', '1.3.0'
|
|
30
29
|
|
|
31
30
|
s.add_dependency 'json'
|
|
32
31
|
s.add_dependency 'logger'
|
data/test/app/about/_error.rb
CHANGED
data/test/app/api+.rb
CHANGED
data/test/app_custom/_site.rb
CHANGED
data/test/bm_router_proc.rb
CHANGED
|
@@ -13,7 +13,7 @@ require 'roda'
|
|
|
13
13
|
require 'benchmark/ips'
|
|
14
14
|
require 'securerandom'
|
|
15
15
|
require 'rack/mock_request'
|
|
16
|
-
require '
|
|
16
|
+
require 'syntropy/request/mock_adapter'
|
|
17
17
|
|
|
18
18
|
class BM
|
|
19
19
|
def self.name(name)
|
|
@@ -100,7 +100,7 @@ p roda_app.(req)
|
|
|
100
100
|
|
|
101
101
|
################################################################################
|
|
102
102
|
|
|
103
|
-
class
|
|
103
|
+
class Syntropy::Request
|
|
104
104
|
def response_headers
|
|
105
105
|
adapter.headers
|
|
106
106
|
end
|
|
@@ -157,7 +157,7 @@ proc = ->(req) { syntropy_app.(req) }
|
|
|
157
157
|
|
|
158
158
|
module ::Kernel
|
|
159
159
|
def mock_req(headers, body = nil)
|
|
160
|
-
|
|
160
|
+
Syntropy::MockAdapter.mock(headers, body)
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
163
|
|
data/test/helper.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'bundler/setup'
|
|
|
4
4
|
require_relative './coverage' if ENV['COVERAGE']
|
|
5
5
|
require 'uringmachine'
|
|
6
6
|
require 'syntropy'
|
|
7
|
-
require '
|
|
7
|
+
require 'syntropy/request/mock_adapter'
|
|
8
8
|
require 'minitest/autorun'
|
|
9
9
|
require 'fileutils'
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ end
|
|
|
19
19
|
|
|
20
20
|
module ::Kernel
|
|
21
21
|
def mock_req(headers, body = nil)
|
|
22
|
-
|
|
22
|
+
Syntropy::MockAdapter.mock(headers, body)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def capture_exception
|
|
@@ -89,15 +89,16 @@ module Minitest::Assertions
|
|
|
89
89
|
return unless exp_content_type
|
|
90
90
|
|
|
91
91
|
if Symbol === exp_content_type
|
|
92
|
-
exp_content_type =
|
|
92
|
+
exp_content_type = Syntropy::MimeTypes[exp_content_type]
|
|
93
93
|
end
|
|
94
94
|
actual = req.response_content_type
|
|
95
|
+
|
|
95
96
|
assert_equal exp_content_type, actual
|
|
96
97
|
end
|
|
97
98
|
end
|
|
98
99
|
|
|
99
|
-
# Extensions to be used in conjunction with `
|
|
100
|
-
class
|
|
100
|
+
# Extensions to be used in conjunction with `Syntropy::TestAdapter`
|
|
101
|
+
class Syntropy::Request
|
|
101
102
|
def response_headers
|
|
102
103
|
adapter.response_headers
|
|
103
104
|
end
|