qeweney 0.3 → 0.7.1
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/CHANGELOG.md +17 -0
- data/Gemfile.lock +3 -1
- data/TODO.md +72 -25
- data/examples/routing_benchmark.rb +151 -0
- data/lib/qeweney.rb +1 -1
- data/lib/qeweney/rack.rb +104 -0
- data/lib/qeweney/request.rb +1 -0
- data/lib/qeweney/request_info.rb +13 -0
- data/lib/qeweney/response.rb +128 -4
- data/lib/qeweney/routing.rb +53 -13
- data/lib/qeweney/version.rb +1 -1
- data/qeweney.gemspec +1 -0
- data/test/helper.rb +22 -2
- data/test/{test_request.rb → test_request_info.rb} +0 -0
- data/test/test_response.rb +185 -0
- data/test/test_routing.rb +110 -0
- metadata +21 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fcbcd99fda88d91935f36f988b58a066c93b3e59e902efc96c332d95502d0ee4
|
4
|
+
data.tar.gz: 617337fac70cfb6a7a30dfaa6e52f2b7614fbd6c46da2efd5074bfe736666b6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34b0ebde4bcb035be97f8125afd6cf74f05d90fbb1c1fb62635c6f738d74f9daad5b099a697d189b7a068606dc77f698aac6a9566bdb856800d82d70fe782d88
|
7
|
+
data.tar.gz: 78b9df279e496504fb5d36aca3f3ba3c4b6fe6667a14f3afdad6815b08eaa9b582e052ea0ec3a953e15f7be1bfd51c8cbb3ed7c4634c75cbd6d7ae8b83b017ed
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## 0.7.1 20921-03-04
|
2
|
+
|
3
|
+
- Add `#route_relative_path` method
|
4
|
+
|
5
|
+
## 0.6 2021-03-03
|
6
|
+
|
7
|
+
- More work on routing (still WIP)
|
8
|
+
- Implement two-way Rack adapters
|
9
|
+
|
10
|
+
## 0.5 2021-02-15
|
11
|
+
|
12
|
+
- Implement upgrade and WebSocket upgrade responses
|
13
|
+
|
14
|
+
## 0.4 2021-02-12
|
15
|
+
|
16
|
+
- Implement caching and compression for serving static files
|
17
|
+
|
1
18
|
## 0.3 2021-02-12
|
2
19
|
|
3
20
|
- Implement `Request#serve_io`, `Request#serve_file`, `Request#redirect`
|
data/Gemfile.lock
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
qeweney (0.
|
4
|
+
qeweney (0.7.1)
|
5
5
|
escape_utils (~> 1.2.1)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
10
|
ansi (1.5.0)
|
11
|
+
benchmark-ips (2.8.4)
|
11
12
|
builder (3.2.4)
|
12
13
|
escape_utils (1.2.1)
|
13
14
|
minitest (5.11.3)
|
@@ -23,6 +24,7 @@ PLATFORMS
|
|
23
24
|
ruby
|
24
25
|
|
25
26
|
DEPENDENCIES
|
27
|
+
benchmark-ips (~> 2.8.3)
|
26
28
|
minitest (~> 5.11.3)
|
27
29
|
minitest-reporters (~> 1.4.2)
|
28
30
|
qeweney!
|
data/TODO.md
CHANGED
@@ -3,32 +3,79 @@
|
|
3
3
|
- `#serve_file(path, opts)`
|
4
4
|
- See here: https://golang.org/pkg/net/http/#ServeFile
|
5
5
|
- support for `Range` header
|
6
|
-
- support for caching (specified in opts)
|
7
|
-
- support for serving from routing path:
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
req.route do
|
11
|
-
req.on 'assets' do
|
12
|
-
req.serve_file(req.routing_path, base_path: STATIC_PATH)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
# or convenience method
|
17
|
-
req.route do
|
18
|
-
req.on_static_route 'assets', base_path: STATIC_PATH
|
19
|
-
end
|
20
|
-
```
|
21
6
|
|
22
7
|
- `#serve_content(io, opts)`
|
23
8
|
- See here: https://golang.org/pkg/net/http/#ServeContent
|
24
9
|
- support for `Range` header
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
10
|
+
|
11
|
+
## route on host
|
12
|
+
|
13
|
+
- `#on_host`:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
req.route do
|
17
|
+
req.on_host 'example.com' do
|
18
|
+
req.redirect "https://www.example.com#{req.uri}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
- `#on_http`:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
req.route do
|
27
|
+
req.on_http do
|
28
|
+
req.redirect "https://#{req.host}#{req.uri}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
- shorthand:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
req.route do
|
37
|
+
req.on_http { req.redirect_to_https }
|
38
|
+
req.on_host 'example.com' do
|
39
|
+
req.redirect_to_host('www.example.com')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
## templates
|
45
|
+
|
46
|
+
- needs to be pluggable - allow any kind of template
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
WEBSITE_PATH = File.join(__dir__, 'docs')
|
50
|
+
STATIC_PATH = File.join(WEBSITE_PATH, 'static')
|
51
|
+
LAYOUTS_PATH = File.join(WEBSITE_PATH, '_layouts')
|
52
|
+
|
53
|
+
PAGES = Tipi::PageManager(
|
54
|
+
base_path: WEBSITE_PATH,
|
55
|
+
engine: :markdown,
|
56
|
+
layouts: LAYOUTS_PATH
|
57
|
+
)
|
58
|
+
|
59
|
+
app = Tipi.app do |r|
|
60
|
+
r.on 'static' do
|
61
|
+
r.serve_file(r.route_path, base_path: ASSETS_PATH)
|
62
|
+
end
|
63
|
+
|
64
|
+
r.default do
|
65
|
+
PAGES.serve(r)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
## Rewriting URLs
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
app = Tipi.app do |r|
|
74
|
+
r.rewrite '/' => '/docs/index.html'
|
75
|
+
r.rewrite '/docs' => '/docs/'
|
76
|
+
r.rewrite '/docs/' => '/docs/index.html'
|
77
|
+
|
78
|
+
# or maybe
|
79
|
+
r.on '/docs/'
|
80
|
+
end
|
81
|
+
```
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'qeweney'
|
5
|
+
require 'benchmark/ips'
|
6
|
+
|
7
|
+
module Qeweney
|
8
|
+
class MockAdapter
|
9
|
+
attr_reader :calls
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@calls = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(sym, *args)
|
16
|
+
calls << [sym, *args]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.mock(headers = {})
|
21
|
+
Request.new(headers, MockAdapter.new)
|
22
|
+
end
|
23
|
+
|
24
|
+
class Request
|
25
|
+
def response_calls
|
26
|
+
adapter.calls
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_mock_request
|
32
|
+
Qeweney.mock(':path' => '/hello/world', ':method' => 'post')
|
33
|
+
end
|
34
|
+
|
35
|
+
CTApp = ->(r) do
|
36
|
+
r.route do
|
37
|
+
r.on_root { r.redirect '/hello' }
|
38
|
+
r.on('hello') do
|
39
|
+
r.on_get('world') { r.respond 'Hello world' }
|
40
|
+
r.on_get { r.respond 'Hello' }
|
41
|
+
r.on_post do
|
42
|
+
# puts 'Someone said Hello'
|
43
|
+
r.redirect '/'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
FlowControlApp = ->(r) do
|
50
|
+
if r.path == '/'
|
51
|
+
return r.redirect '/hello'
|
52
|
+
elsif r.current_path_part == 'hello'
|
53
|
+
r.enter_route
|
54
|
+
if r.method == 'get' && r.current_path_part == 'world'
|
55
|
+
return r.respond('Hello world')
|
56
|
+
elsif r.method == 'get'
|
57
|
+
return r.respond('Hello')
|
58
|
+
elsif r.method == 'post'
|
59
|
+
return r.redirect('/')
|
60
|
+
end
|
61
|
+
r.leave_route
|
62
|
+
end
|
63
|
+
r.respond(nil, ':status' => 404)
|
64
|
+
end
|
65
|
+
|
66
|
+
class Qeweney::Request
|
67
|
+
def get?
|
68
|
+
method == 'get'
|
69
|
+
end
|
70
|
+
|
71
|
+
def post?
|
72
|
+
method == 'post'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
NicerFlowControlApp = ->(r) do
|
77
|
+
case r.current_path_part
|
78
|
+
when '/'
|
79
|
+
return r.redirect('/hello')
|
80
|
+
when 'hello'
|
81
|
+
r.enter_route
|
82
|
+
if r.get? && r.current_route == 'world'
|
83
|
+
return r.respond('Hello world')
|
84
|
+
elsif r.get?
|
85
|
+
return r.respond('Hello')
|
86
|
+
elsif r.post?
|
87
|
+
return r.redirect('/')
|
88
|
+
end
|
89
|
+
r.leave_route
|
90
|
+
end
|
91
|
+
r.respond(nil, ':status' => 404)
|
92
|
+
end
|
93
|
+
|
94
|
+
OptimizedRubyApp = ->(r) do
|
95
|
+
path = r.path
|
96
|
+
if path == '/'
|
97
|
+
return r.redirect('/hello')
|
98
|
+
elsif path =~ /^\/hello(.+)/
|
99
|
+
method = r.method
|
100
|
+
if method == 'get'
|
101
|
+
rest = Regexp.last_match(1)
|
102
|
+
if rest == '/world'
|
103
|
+
return r.respond('Hello world')
|
104
|
+
else
|
105
|
+
return r.respond('Hello')
|
106
|
+
end
|
107
|
+
elsif method == 'post'
|
108
|
+
# puts 'Someone said Hello'
|
109
|
+
return r.redirect('/')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
r.respond(nil, ':status' => 404)
|
113
|
+
end
|
114
|
+
|
115
|
+
def test
|
116
|
+
r = create_mock_request
|
117
|
+
puts '* catch/throw'
|
118
|
+
CTApp.call(r)
|
119
|
+
p r.response_calls
|
120
|
+
|
121
|
+
r = create_mock_request
|
122
|
+
puts '* classic flow control'
|
123
|
+
FlowControlApp.call(r)
|
124
|
+
p r.response_calls
|
125
|
+
|
126
|
+
r = create_mock_request
|
127
|
+
puts '* nicer flow control'
|
128
|
+
NicerFlowControlApp.call(r)
|
129
|
+
p r.response_calls
|
130
|
+
|
131
|
+
r = create_mock_request
|
132
|
+
puts '* optimized Ruby'
|
133
|
+
OptimizedRubyApp.call(r)
|
134
|
+
p r.response_calls
|
135
|
+
end
|
136
|
+
|
137
|
+
def benchmark
|
138
|
+
Benchmark.ips do |x|
|
139
|
+
x.config(:time => 3, :warmup => 1)
|
140
|
+
|
141
|
+
x.report("catch/throw") { CTApp.call(create_mock_request) }
|
142
|
+
x.report("flow control") { FlowControlApp.call(create_mock_request) }
|
143
|
+
x.report("nicer flow control") { NicerFlowControlApp.call(create_mock_request) }
|
144
|
+
x.report("hand-optimized") { OptimizedRubyApp.call(create_mock_request) }
|
145
|
+
|
146
|
+
x.compare!
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
test
|
151
|
+
benchmark
|
data/lib/qeweney.rb
CHANGED
data/lib/qeweney/rack.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qeweney
|
4
|
+
class RackRequestAdapter
|
5
|
+
def initialize(env)
|
6
|
+
@env = env
|
7
|
+
@response_headers = {}
|
8
|
+
@response_body = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def request_headers
|
12
|
+
request_http_headers.merge(
|
13
|
+
':scheme' => @env['rack.url_scheme'],
|
14
|
+
':method' => @env['REQUEST_METHOD'].downcase,
|
15
|
+
':path' => request_path_from_env
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def request_path_from_env
|
20
|
+
path = File.join(@env['SCRIPT_NAME'], @env['PATH_INFO'])
|
21
|
+
path = path + "?#{@env['QUERY_STRING']}" if @env['QUERY_STRING']
|
22
|
+
path
|
23
|
+
end
|
24
|
+
|
25
|
+
def request_http_headers
|
26
|
+
headers = {}
|
27
|
+
@env.each do |k, v|
|
28
|
+
next unless k =~ /^HTTP_(.+)$/
|
29
|
+
|
30
|
+
headers[Regexp.last_match(1).downcase.gsub('_', '-')] = v
|
31
|
+
end
|
32
|
+
headers
|
33
|
+
end
|
34
|
+
|
35
|
+
def respond(body, headers)
|
36
|
+
@response_body << body
|
37
|
+
@response_headers = headers
|
38
|
+
end
|
39
|
+
|
40
|
+
def send_headers(headers, empty_response: nil)
|
41
|
+
@response_headers = headers
|
42
|
+
end
|
43
|
+
|
44
|
+
def send_chunk(body, done: false)
|
45
|
+
@response_body << body
|
46
|
+
end
|
47
|
+
|
48
|
+
def finish
|
49
|
+
end
|
50
|
+
|
51
|
+
def rack_response
|
52
|
+
@status = @response_headers.delete(':status')
|
53
|
+
[
|
54
|
+
@status,
|
55
|
+
@response_headers,
|
56
|
+
@response_body
|
57
|
+
]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.rack(&block)
|
62
|
+
proc do |env|
|
63
|
+
adapter = RackRequestAdapter.new(env)
|
64
|
+
req = Request.new(adapter.request_headers, adapter)
|
65
|
+
block.(req)
|
66
|
+
adapter.rack_response
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.rack_env_from_request(request)
|
71
|
+
Hash.new do |h, k|
|
72
|
+
h[k] = env_value_from_request(request, k)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
RACK_ENV = {
|
77
|
+
'SCRIPT_NAME' => '',
|
78
|
+
'rack.version' => [1, 3],
|
79
|
+
'SERVER_PORT' => '80', # ?
|
80
|
+
'rack.url_scheme' => 'http', # ?
|
81
|
+
'rack.errors' => STDERR, # ?
|
82
|
+
'rack.multithread' => false,
|
83
|
+
'rack.run_once' => false,
|
84
|
+
'rack.hijack?' => false,
|
85
|
+
'rack.hijack' => nil,
|
86
|
+
'rack.hijack_io' => nil,
|
87
|
+
'rack.session' => nil,
|
88
|
+
'rack.logger' => nil,
|
89
|
+
'rack.multipart.buffer_size' => nil,
|
90
|
+
'rack.multipar.tempfile_factory' => nil
|
91
|
+
}
|
92
|
+
|
93
|
+
def self.env_value_from_request(request, key)
|
94
|
+
case key
|
95
|
+
when 'REQUEST_METHOD' then request.method
|
96
|
+
when 'PATH_INFO' then request.path
|
97
|
+
when 'QUERY_STRING' then request.query_string || ''
|
98
|
+
when 'SERVER_NAME' then request.headers['host']
|
99
|
+
when 'rack.input' then InputStream.new(request)
|
100
|
+
when HTTP_HEADER_RE then request.headers[$1.gsub('_', '-').downcase]
|
101
|
+
else RACK_ENV[key]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/qeweney/request.rb
CHANGED
data/lib/qeweney/request_info.rb
CHANGED
@@ -17,6 +17,10 @@ module Qeweney
|
|
17
17
|
connection == 'upgrade' && @headers['upgrade']&.downcase
|
18
18
|
end
|
19
19
|
|
20
|
+
def websocket_version
|
21
|
+
headers['sec-websocket-version'].to_i
|
22
|
+
end
|
23
|
+
|
20
24
|
def protocol
|
21
25
|
@protocol ||= @adapter.protocol
|
22
26
|
end
|
@@ -61,6 +65,15 @@ module Qeweney
|
|
61
65
|
def forwarded_for
|
62
66
|
@headers['x-forwarded-for']
|
63
67
|
end
|
68
|
+
|
69
|
+
# TODO: should return encodings in client's order of preference (and take
|
70
|
+
# into account q weights)
|
71
|
+
def accept_encoding
|
72
|
+
encoding = @headers['accept-encoding']
|
73
|
+
return [] unless encoding
|
74
|
+
|
75
|
+
encoding.split(',').map { |i| i.strip }
|
76
|
+
end
|
64
77
|
end
|
65
78
|
|
66
79
|
module RequestInfoClassMethods
|
data/lib/qeweney/response.rb
CHANGED
@@ -1,21 +1,113 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'time'
|
4
|
+
require 'zlib'
|
5
|
+
require 'stringio'
|
6
|
+
require 'digest/sha1'
|
7
|
+
|
3
8
|
require_relative 'status'
|
4
9
|
|
5
10
|
module Qeweney
|
11
|
+
module StaticFileCaching
|
12
|
+
class << self
|
13
|
+
def file_stat_to_etag(stat)
|
14
|
+
"#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def file_stat_to_last_modified(stat)
|
18
|
+
stat.mtime.httpdate
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
6
23
|
module ResponseMethods
|
24
|
+
def upgrade(protocol, custom_headers = nil)
|
25
|
+
upgrade_headers = {
|
26
|
+
':status' => Status::SWITCHING_PROTOCOLS,
|
27
|
+
'Upgrade' => protocol,
|
28
|
+
'Connection' => 'upgrade'
|
29
|
+
}
|
30
|
+
upgrade_headers.merge!(custom_headers) if custom_headers
|
31
|
+
|
32
|
+
respond(nil, upgrade_headers)
|
33
|
+
end
|
34
|
+
|
35
|
+
WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
36
|
+
|
37
|
+
def upgrade_to_websocket(custom_headers = nil)
|
38
|
+
key = "#{headers['sec-websocket-key']}#{WEBSOCKET_GUID}"
|
39
|
+
upgrade_headers = {
|
40
|
+
'Sec-WebSocket-Accept' => Digest::SHA1.base64digest(key)
|
41
|
+
}
|
42
|
+
upgrade_headers.merge!(custom_headers) if custom_headers
|
43
|
+
upgrade('websocket', upgrade_headers)
|
44
|
+
|
45
|
+
adapter.websocket_connection(self)
|
46
|
+
end
|
47
|
+
|
7
48
|
def redirect(url, status = Status::FOUND)
|
8
49
|
respond(nil, ':status' => status, 'Location' => url)
|
9
50
|
end
|
10
51
|
|
52
|
+
def redirect_to_https(status = Status::MOVED_PERMANENTLY)
|
53
|
+
secure_uri = "https://#{host}#{uri}"
|
54
|
+
redirect(secure_uri, status)
|
55
|
+
end
|
56
|
+
|
57
|
+
def redirect_to_host(new_host, status = Status::FOUND)
|
58
|
+
secure_uri = "//#{new_host}#{uri}"
|
59
|
+
redirect(secure_uri, status)
|
60
|
+
end
|
61
|
+
|
11
62
|
def serve_file(path, opts)
|
12
|
-
|
13
|
-
|
63
|
+
full_path = file_full_path(path, opts)
|
64
|
+
stat = File.stat(full_path)
|
65
|
+
etag = StaticFileCaching.file_stat_to_etag(stat)
|
66
|
+
last_modified = StaticFileCaching.file_stat_to_last_modified(stat)
|
67
|
+
|
68
|
+
if validate_static_file_cache(etag, last_modified)
|
69
|
+
return respond(nil, {
|
70
|
+
':status' => Status::NOT_MODIFIED,
|
71
|
+
'etag' => etag
|
72
|
+
})
|
14
73
|
end
|
15
|
-
|
74
|
+
|
75
|
+
respond_with_static_file(full_path, etag, last_modified, opts)
|
76
|
+
rescue Errno::ENOENT => e
|
16
77
|
respond(nil, ':status' => Status::NOT_FOUND)
|
17
78
|
end
|
18
79
|
|
80
|
+
def respond_with_static_file(path, etag, last_modified, opts)
|
81
|
+
File.open(path, 'r') do |f|
|
82
|
+
opts = opts.merge(headers: {
|
83
|
+
'etag' => etag,
|
84
|
+
'last-modified' => last_modified,
|
85
|
+
})
|
86
|
+
|
87
|
+
# accept_encoding should return encodings in client's order of preference
|
88
|
+
accept_encoding.each do |encoding|
|
89
|
+
case encoding
|
90
|
+
when 'deflate'
|
91
|
+
return serve_io_deflate(f, opts)
|
92
|
+
when 'gzip'
|
93
|
+
return serve_io_gzip(f, opts)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
serve_io(f, opts)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_static_file_cache(etag, last_modified)
|
101
|
+
if (none_match = headers['if-none-match'])
|
102
|
+
return true if none_match == etag
|
103
|
+
end
|
104
|
+
if (modified_since = headers['if-modified-since'])
|
105
|
+
return true if modified_since == last_modified
|
106
|
+
end
|
107
|
+
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
19
111
|
def file_full_path(path, opts)
|
20
112
|
if (base_path = opts[:base_path])
|
21
113
|
File.join(opts[:base_path], path)
|
@@ -25,7 +117,39 @@ module Qeweney
|
|
25
117
|
end
|
26
118
|
|
27
119
|
def serve_io(io, opts)
|
28
|
-
respond(io.read)
|
120
|
+
respond(io.read, opts[:headers] || {})
|
121
|
+
end
|
122
|
+
|
123
|
+
def serve_io_deflate(io, opts)
|
124
|
+
deflate = Zlib::Deflate.new
|
125
|
+
headers = opts[:headers].merge(
|
126
|
+
'content-encoding' => 'deflate',
|
127
|
+
'vary' => 'Accept-Encoding'
|
128
|
+
)
|
129
|
+
|
130
|
+
respond(deflate.deflate(io.read, Zlib::FINISH), headers)
|
131
|
+
end
|
132
|
+
|
133
|
+
def serve_io_gzip(io, opts)
|
134
|
+
buf = StringIO.new
|
135
|
+
z = Zlib::GzipWriter.new(buf)
|
136
|
+
z << io.read
|
137
|
+
z.flush
|
138
|
+
z.close
|
139
|
+
headers = opts[:headers].merge(
|
140
|
+
'content-encoding' => 'gzip',
|
141
|
+
'vary' => 'Accept-Encoding'
|
142
|
+
)
|
143
|
+
respond(buf.string, headers)
|
144
|
+
end
|
145
|
+
|
146
|
+
def serve_rack(app)
|
147
|
+
response = app.(Qeweney.rack_env_from_request(self))
|
148
|
+
headers = (response[1] || {}).merge(':status' => response[0])
|
149
|
+
respond(response[2].join, headers)
|
150
|
+
|
151
|
+
# TODO: send separate chunks for multi-part body
|
152
|
+
# TODO: add support for streaming body
|
29
153
|
end
|
30
154
|
end
|
31
155
|
end
|
data/lib/qeweney/routing.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Qeweney
|
4
|
+
def self.route(&block)
|
5
|
+
->(r) { r.route(&block) }
|
6
|
+
end
|
7
|
+
|
4
8
|
module RoutingMethods
|
5
9
|
def route(&block)
|
10
|
+
(@path_parts ||= path.split('/'))[@path_parts_idx ||= 1]
|
6
11
|
res = catch(:stop) { yield self }
|
7
12
|
return if res == :found
|
8
13
|
|
@@ -11,36 +16,57 @@ module Qeweney
|
|
11
16
|
|
12
17
|
def route_found(&block)
|
13
18
|
catch(:stop, &block)
|
14
|
-
throw :stop, :found
|
19
|
+
throw :stop, headers_sent? ? :found : nil
|
15
20
|
end
|
16
21
|
|
17
22
|
@@regexp_cache = {}
|
18
23
|
|
19
|
-
def
|
20
|
-
@
|
24
|
+
def route_part
|
25
|
+
@path_parts[@path_parts_idx]
|
21
26
|
end
|
22
|
-
|
23
|
-
def on(route = nil, &block)
|
24
|
-
@__routing_path__ ||= path
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
def route_relative_path
|
29
|
+
@path_parts.empty? ? '/' : @path_parts[@path_parts_idx..-1].join('/')
|
30
|
+
end
|
31
|
+
|
32
|
+
def enter_route
|
33
|
+
@path_parts_idx += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def leave_route
|
37
|
+
@path_parts_idx -= 1
|
38
|
+
end
|
29
39
|
|
30
|
-
|
40
|
+
def on(route = nil, &block)
|
41
|
+
if route
|
42
|
+
return unless @path_parts[@path_parts_idx] == route
|
31
43
|
end
|
32
44
|
|
45
|
+
enter_route
|
33
46
|
route_found(&block)
|
47
|
+
leave_route
|
34
48
|
end
|
35
49
|
|
36
50
|
def is(route = '/', &block)
|
37
|
-
return unless @
|
51
|
+
return unless @path_parts[@path_parts_idx] == route && @path_parts_idx >= @path_parts.size
|
38
52
|
|
39
53
|
route_found(&block)
|
40
54
|
end
|
41
55
|
|
42
56
|
def on_root(&block)
|
43
|
-
return unless @
|
57
|
+
return unless @path_parts_idx > @path_parts.size - 1
|
58
|
+
|
59
|
+
route_found(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_host(route, &block)
|
63
|
+
return unless host == route
|
64
|
+
|
65
|
+
route_found(&block)
|
66
|
+
end
|
67
|
+
|
68
|
+
def on_plain_http(route, &block)
|
69
|
+
return unless scheme == 'http'
|
44
70
|
|
45
71
|
route_found(&block)
|
46
72
|
end
|
@@ -86,8 +112,22 @@ module Qeweney
|
|
86
112
|
route_found(&block)
|
87
113
|
end
|
88
114
|
|
115
|
+
def on_upgrade(protocol, &block)
|
116
|
+
return unless upgrade_protocol == protocol
|
117
|
+
|
118
|
+
route_found(&block)
|
119
|
+
end
|
120
|
+
|
121
|
+
def on_websocket_upgrade(&block)
|
122
|
+
on_upgrade('websocket', &block)
|
123
|
+
end
|
124
|
+
|
89
125
|
def stop_routing
|
90
|
-
|
126
|
+
throw :stop, :found
|
127
|
+
end
|
128
|
+
|
129
|
+
def default
|
130
|
+
yield
|
91
131
|
throw :stop, :found
|
92
132
|
end
|
93
133
|
end
|
data/lib/qeweney/version.rb
CHANGED
data/qeweney.gemspec
CHANGED
data/test/helper.rb
CHANGED
@@ -11,8 +11,28 @@ require 'minitest/autorun'
|
|
11
11
|
require 'minitest/reporters'
|
12
12
|
|
13
13
|
module Qeweney
|
14
|
-
|
15
|
-
|
14
|
+
class MockAdapter
|
15
|
+
attr_reader :calls
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@calls = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(sym, *args)
|
22
|
+
calls << [sym, *args]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.mock(headers = {})
|
27
|
+
headers[':method'] ||= ''
|
28
|
+
headers[':path'] ||= ''
|
29
|
+
Request.new(headers, MockAdapter.new)
|
30
|
+
end
|
31
|
+
|
32
|
+
class Request
|
33
|
+
def response_calls
|
34
|
+
adapter.calls
|
35
|
+
end
|
16
36
|
end
|
17
37
|
end
|
18
38
|
|
File without changes
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
class RedirectTest < MiniTest::Test
|
6
|
+
def test_redirect
|
7
|
+
r = Qeweney.mock
|
8
|
+
r.redirect('/foo')
|
9
|
+
|
10
|
+
assert_equal [
|
11
|
+
[:respond, nil, {":status"=>302, "Location"=>"/foo"}]
|
12
|
+
], r.response_calls
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_redirect_wirth_status
|
16
|
+
r = Qeweney.mock
|
17
|
+
r.redirect('/bar', Qeweney::Status::MOVED_PERMANENTLY)
|
18
|
+
|
19
|
+
assert_equal [
|
20
|
+
[:respond, nil, {":status"=>301, "Location"=>"/bar"}]
|
21
|
+
], r.response_calls
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class StaticFileResponeTest < MiniTest::Test
|
26
|
+
def setup
|
27
|
+
@path = File.join(__dir__, 'helper.rb')
|
28
|
+
@stat = File.stat(@path)
|
29
|
+
|
30
|
+
@etag = Qeweney::StaticFileCaching.file_stat_to_etag(@stat)
|
31
|
+
@last_modified = Qeweney::StaticFileCaching.file_stat_to_last_modified(@stat)
|
32
|
+
@content = IO.read(@path)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_serve_file
|
36
|
+
r = Qeweney.mock
|
37
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
38
|
+
|
39
|
+
assert_equal [
|
40
|
+
[:respond, @content, { 'etag' => @etag, 'last-modified' => @last_modified }]
|
41
|
+
], r.response_calls
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_serve_file_with_cache_headers
|
45
|
+
r = Qeweney.mock('if-none-match' => @etag)
|
46
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
47
|
+
assert_equal [
|
48
|
+
[:respond, nil, { 'etag' => @etag, ':status' => Qeweney::Status::NOT_MODIFIED }]
|
49
|
+
], r.response_calls
|
50
|
+
|
51
|
+
r = Qeweney.mock('if-modified-since' => @last_modified)
|
52
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
53
|
+
assert_equal [
|
54
|
+
[:respond, nil, { 'etag' => @etag, ':status' => Qeweney::Status::NOT_MODIFIED }]
|
55
|
+
], r.response_calls
|
56
|
+
|
57
|
+
r = Qeweney.mock('if-none-match' => 'foobar')
|
58
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
59
|
+
assert_equal [
|
60
|
+
[:respond, @content, { 'etag' => @etag, 'last-modified' => @last_modified }]
|
61
|
+
], r.response_calls
|
62
|
+
|
63
|
+
r = Qeweney.mock('if-modified-since' => Time.now.httpdate)
|
64
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
65
|
+
assert_equal [
|
66
|
+
[:respond, @content, { 'etag' => @etag, 'last-modified' => @last_modified }]
|
67
|
+
], r.response_calls
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_serve_file_deflate
|
71
|
+
r = Qeweney.mock('accept-encoding' => 'deflate')
|
72
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
73
|
+
|
74
|
+
deflate = Zlib::Deflate.new
|
75
|
+
deflated_content = deflate.deflate(@content, Zlib::FINISH)
|
76
|
+
|
77
|
+
assert_equal [
|
78
|
+
[:respond, deflated_content, {
|
79
|
+
'etag' => @etag,
|
80
|
+
'last-modified' => @last_modified,
|
81
|
+
'vary' => 'Accept-Encoding',
|
82
|
+
'content-encoding' => 'deflate'
|
83
|
+
}]
|
84
|
+
], r.response_calls
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_serve_file_gzip
|
88
|
+
r = Qeweney.mock('accept-encoding' => 'gzip')
|
89
|
+
r.serve_file('helper.rb', base_path: __dir__)
|
90
|
+
|
91
|
+
buf = StringIO.new
|
92
|
+
z = Zlib::GzipWriter.new(buf)
|
93
|
+
z << @content
|
94
|
+
z.flush
|
95
|
+
z.close
|
96
|
+
gzipped_content = buf.string
|
97
|
+
|
98
|
+
assert_equal [
|
99
|
+
[:respond, gzipped_content, {
|
100
|
+
'etag' => @etag,
|
101
|
+
'last-modified' => @last_modified,
|
102
|
+
'vary' => 'Accept-Encoding',
|
103
|
+
'content-encoding' => 'gzip'
|
104
|
+
}]
|
105
|
+
], r.response_calls
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_serve_file_non_existent
|
109
|
+
r = Qeweney.mock
|
110
|
+
r.serve_file('foo.rb', base_path: __dir__)
|
111
|
+
assert_equal [
|
112
|
+
[:respond, nil, { ':status' => Qeweney::Status::NOT_FOUND }]
|
113
|
+
], r.response_calls
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class UpgradeTest < MiniTest::Test
|
118
|
+
def test_upgrade
|
119
|
+
r = Qeweney.mock
|
120
|
+
r.upgrade('df')
|
121
|
+
|
122
|
+
assert_equal [
|
123
|
+
[:respond, nil, {
|
124
|
+
':status' => 101,
|
125
|
+
'Upgrade' => 'df',
|
126
|
+
'Connection' => 'upgrade'
|
127
|
+
}]
|
128
|
+
], r.response_calls
|
129
|
+
|
130
|
+
|
131
|
+
r = Qeweney.mock
|
132
|
+
r.upgrade('df', { 'foo' => 'bar' })
|
133
|
+
|
134
|
+
assert_equal [
|
135
|
+
[:respond, nil, {
|
136
|
+
':status' => 101,
|
137
|
+
'Upgrade' => 'df',
|
138
|
+
'Connection' => 'upgrade',
|
139
|
+
'foo' => 'bar'
|
140
|
+
}]
|
141
|
+
], r.response_calls
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_websocket_upgrade
|
145
|
+
r = Qeweney.mock(
|
146
|
+
'connection' => 'upgrade',
|
147
|
+
'upgrade' => 'websocket',
|
148
|
+
'sec-websocket-version' => '23',
|
149
|
+
'sec-websocket-key' => 'abcdefghij'
|
150
|
+
)
|
151
|
+
|
152
|
+
assert_equal 'websocket', r.upgrade_protocol
|
153
|
+
|
154
|
+
r.upgrade_to_websocket('foo' => 'baz')
|
155
|
+
accept = Digest::SHA1.base64digest('abcdefghij258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
156
|
+
|
157
|
+
assert_equal [
|
158
|
+
[:respond, nil, {
|
159
|
+
':status' => 101,
|
160
|
+
'Upgrade' => 'websocket',
|
161
|
+
'Connection' => 'upgrade',
|
162
|
+
'foo' => 'baz',
|
163
|
+
'Sec-WebSocket-Accept' => accept
|
164
|
+
}],
|
165
|
+
[:websocket_connection, r]
|
166
|
+
], r.response_calls
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class ServeRackTest < MiniTest::Test
|
171
|
+
def test_serve_rack
|
172
|
+
r = Qeweney.mock(
|
173
|
+
':method' => 'get',
|
174
|
+
':path' => '/foo/bar?a=1&b=2%2F3',
|
175
|
+
'accept' => 'blah'
|
176
|
+
)
|
177
|
+
r.serve_rack(->(env) {
|
178
|
+
[404, {'Foo' => 'Bar'}, ["#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"]]
|
179
|
+
})
|
180
|
+
|
181
|
+
assert_equal [
|
182
|
+
[:respond, "get /foo/bar", {':status' => 404, 'Foo' => 'Bar' }]
|
183
|
+
], r.response_calls
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
class RoutingTest < MiniTest::Test
|
6
|
+
App1 = ->(r) do
|
7
|
+
r.route do
|
8
|
+
r.on_root { r.redirect '/hello' }
|
9
|
+
r.on('hello') do
|
10
|
+
r.on_get('world') { r.respond 'Hello world' }
|
11
|
+
r.on_get { r.respond 'Hello' }
|
12
|
+
r.on_post do
|
13
|
+
r.redirect '/'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_app1
|
20
|
+
r = Qeweney.mock(':path' => '/foo')
|
21
|
+
App1.(r)
|
22
|
+
assert_equal [[:respond, nil, { ':status' => 404 }]], r.response_calls
|
23
|
+
|
24
|
+
r = Qeweney.mock(':path' => '/')
|
25
|
+
App1.(r)
|
26
|
+
assert_equal [[:respond, nil, { ':status' => 302, 'Location' => '/hello' }]], r.response_calls
|
27
|
+
|
28
|
+
r = Qeweney.mock(':path' => '/hello', ':method' => 'foo')
|
29
|
+
App1.(r)
|
30
|
+
assert_equal [[:respond, nil, { ':status' => 404 }]], r.response_calls
|
31
|
+
|
32
|
+
r = Qeweney.mock(':path' => '/hello', ':method' => 'get')
|
33
|
+
App1.(r)
|
34
|
+
assert_equal [[:respond, 'Hello', {}]], r.response_calls
|
35
|
+
|
36
|
+
r = Qeweney.mock(':path' => '/hello', ':method' => 'post')
|
37
|
+
App1.(r)
|
38
|
+
assert_equal [[:respond, nil, { ':status' => 302, 'Location' => '/' }]], r.response_calls
|
39
|
+
|
40
|
+
r = Qeweney.mock(':path' => '/hello/world', ':method' => 'get')
|
41
|
+
App1.(r)
|
42
|
+
assert_equal [[:respond, 'Hello world', {}]], r.response_calls
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_on_root
|
46
|
+
app = Qeweney.route do |r|
|
47
|
+
r.on_root { r.respond('root') }
|
48
|
+
r.on('foo') {
|
49
|
+
r.on_root { r.respond('foo root') }
|
50
|
+
r.on('bar') {
|
51
|
+
r.on_root { r.respond('bar root') }
|
52
|
+
r.on('baz') {
|
53
|
+
r.on_root { r.respond('baz root') }
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
r = Qeweney.mock(':path' => '/')
|
60
|
+
app.(r)
|
61
|
+
assert_equal [[:respond, 'root', {}]], r.response_calls
|
62
|
+
|
63
|
+
r = Qeweney.mock(':path' => '/foo')
|
64
|
+
app.(r)
|
65
|
+
assert_equal [[:respond, 'foo root', {}]], r.response_calls
|
66
|
+
|
67
|
+
r = Qeweney.mock(':path' => '/foo/bar')
|
68
|
+
app.(r)
|
69
|
+
assert_equal [[:respond, 'bar root', {}]], r.response_calls
|
70
|
+
|
71
|
+
r = Qeweney.mock(':path' => '/foo/bar/baz')
|
72
|
+
app.(r)
|
73
|
+
assert_equal [[:respond, 'baz root', {}]], r.response_calls
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_relative_path
|
77
|
+
default_relative_path = nil
|
78
|
+
|
79
|
+
app = Qeweney.route do |r|
|
80
|
+
default_relative_path = r.route_relative_path
|
81
|
+
r.on_root { r.respond(File.join('ROOT', r.route_relative_path)) }
|
82
|
+
r.on('foo') { r.respond(File.join('FOO', r.route_relative_path)) }
|
83
|
+
r.on('bar') {
|
84
|
+
r.on('baz') { r.respond(File.join('BAR/BAZ', r.route_relative_path)) }
|
85
|
+
r.default { r.respond(File.join('BAR', r.route_relative_path)) }
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
r = Qeweney.mock(':path' => '/')
|
90
|
+
app.(r)
|
91
|
+
assert_equal '/', default_relative_path
|
92
|
+
assert_equal [[:respond, 'ROOT/', {}]], r.response_calls
|
93
|
+
|
94
|
+
|
95
|
+
r = Qeweney.mock(':path' => '/foo/bar/baz')
|
96
|
+
app.(r)
|
97
|
+
assert_equal 'foo/bar/baz', default_relative_path
|
98
|
+
assert_equal [[:respond, 'FOO/bar/baz', {}]], r.response_calls
|
99
|
+
|
100
|
+
r = Qeweney.mock(':path' => '/bar/a/b/c')
|
101
|
+
app.(r)
|
102
|
+
assert_equal 'bar/a/b/c', default_relative_path
|
103
|
+
assert_equal [[:respond, 'BAR/a/b/c', {}]], r.response_calls
|
104
|
+
|
105
|
+
r = Qeweney.mock(':path' => '/bar/baz/b/c')
|
106
|
+
app.(r)
|
107
|
+
assert_equal 'bar/baz/b/c', default_relative_path
|
108
|
+
assert_equal [[:respond, 'BAR/BAZ/b/c', {}]], r.response_calls
|
109
|
+
end
|
110
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qeweney
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: escape_utils
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 1.4.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: benchmark-ips
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.8.3
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.8.3
|
69
83
|
description:
|
70
84
|
email: sharon@noteflakes.com
|
71
85
|
executables: []
|
@@ -82,8 +96,10 @@ files:
|
|
82
96
|
- README.md
|
83
97
|
- Rakefile
|
84
98
|
- TODO.md
|
99
|
+
- examples/routing_benchmark.rb
|
85
100
|
- lib/qeweney.rb
|
86
101
|
- lib/qeweney/mime_types.rb
|
102
|
+
- lib/qeweney/rack.rb
|
87
103
|
- lib/qeweney/request.rb
|
88
104
|
- lib/qeweney/request_info.rb
|
89
105
|
- lib/qeweney/response.rb
|
@@ -93,7 +109,9 @@ files:
|
|
93
109
|
- qeweney.gemspec
|
94
110
|
- test/helper.rb
|
95
111
|
- test/run.rb
|
96
|
-
- test/
|
112
|
+
- test/test_request_info.rb
|
113
|
+
- test/test_response.rb
|
114
|
+
- test/test_routing.rb
|
97
115
|
homepage: http://github.com/digital-fabric/qeweney
|
98
116
|
licenses:
|
99
117
|
- MIT
|