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.
@@ -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 'qeweney'
4
- require 'json'
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
- # Extensions for the Qeweney::Request class
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' => Qeweney::Status::NOT_MODIFIED)
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
@@ -20,7 +20,7 @@ module Syntropy
20
20
  #
21
21
  lambda { |req|
22
22
  site = sites[req.host]
23
- site ? site.call(req) : req.respond(nil, ':status' => Status::BAD_REQUEST)
23
+ site ? site.call(req) : req.respond(nil, ':status' => HTTP::BAD_REQUEST)
24
24
  }
25
25
  end
26
26
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.29.0'
4
+ VERSION = '0.30.0'
5
5
  end
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/connection'
9
- require 'syntropy/server'
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 'listen', '~>3.9.0'
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'
@@ -1,4 +1,4 @@
1
- DEFAULT_STATUS = Qeweney::Status::INTERNAL_SERVER_ERROR
1
+ DEFAULT_STATUS = HTTP::INTERNAL_SERVER_ERROR
2
2
 
3
3
  export ->(req, err) {
4
4
  status = err.respond_to?(:http_status) ? err.http_status : DEFAULT_STATUS
data/test/app/api+.rb CHANGED
@@ -10,7 +10,7 @@ class API < Syntropy::JSONAPI
10
10
 
11
11
  def incr!(req)
12
12
  if req.path != '/test/api'
13
- raise Syntropy::Error.new('Teapot', Qeweney::Status::TEAPOT)
13
+ raise Syntropy::Error.new('Teapot', HTTP::TEAPOT)
14
14
  end
15
15
 
16
16
  @count += 1
@@ -1,3 +1,3 @@
1
1
  export ->(req) {
2
- req.respond(nil, ':status' => Status::TEAPOT)
2
+ req.respond(nil, ':status' => HTTP::TEAPOT)
3
3
  }
@@ -13,7 +13,7 @@ require 'roda'
13
13
  require 'benchmark/ips'
14
14
  require 'securerandom'
15
15
  require 'rack/mock_request'
16
- require 'qeweney/mock_adapter'
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 Qeweney::Request
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
- Qeweney::MockAdapter.mock(headers, body).tap { it.setup_mock_request }
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 'qeweney/mock_adapter'
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
- Qeweney::MockAdapter.mock(headers, body).tap { it.setup_mock_request }
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 = Qeweney::MimeTypes[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 `Qeweney::TestAdapter`
100
- class Qeweney::Request
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