qeweney 0.4 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfb8f3a7995ccb0d35e79ecb1cbc9f2556ee4bc5e0bc7d61d3f1278395ea47df
4
- data.tar.gz: 04cf0bbb7ebe8a701ab7a7b4e912e461ffc744ece02a1b75ff791eba22036da6
3
+ metadata.gz: 693c85b30e907232ff7bfa77a8c058a8ef1155a108b7b554ecaf68482b6bfe9a
4
+ data.tar.gz: 13a9d1c4d6277f90346193b4c56cae0cf44735947268136a7d853071535b3241
5
5
  SHA512:
6
- metadata.gz: 4f2d77164b052af1cdae718a2c44ccb3c0313bcc73cc87b637b9a1ccabee6701fecd5b223020b4ca854e74d218fefc0101a6150a4e426a9b5c2f1e41a8d6a951
7
- data.tar.gz: 0030e7024d6a42f0301ba515e49f1e8da52e3b65368b515d81b520396a187dc0adf8107b32b8f0d08eb0f62d0e3c6f2f9e34f617a69444211b01b4971d950ed2
6
+ metadata.gz: 9e34c036534ec0899b251628f666e0f5752c10d38bb98c07765a3149ae901be85a56128880889f71148fec4e90aa2324cce78817f311b79b52b9f2cefb2b19c1
7
+ data.tar.gz: ccbd53e40b78a60a5665c187c2dd65b4330c1c68c13eb15209f49551df3b0c35c070251662538a3de63cfb56ae507c170c88e767da95178e896a72d5835492ef
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.7.2 2021-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
+
1
14
  ## 0.4 2021-02-12
2
15
 
3
16
  - Implement caching and compression for serving static files
data/Gemfile.lock CHANGED
@@ -1,13 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- qeweney (0.4)
4
+ qeweney (0.7.2)
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,97 +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
- - support for caching (specified in opts)
26
- - usage:
27
10
 
28
- ```ruby
29
- req.route do
30
- req.on 'mypdf' do
31
- File.open('my.pdf', 'r') { |f| req.serve_content(io) }
32
- end
33
- end
34
- ```
11
+ ## route on host
35
12
 
36
- ## Caching
13
+ - `#on_host`:
37
14
 
38
- ```ruby
39
- req.route do
40
- req.on 'assets' do
41
- # setting cache to true implies the following:
42
- # - etag (calculated from file stat)
43
- # - last-modified (from file stat)
44
- # - vary: Accept-Encoding
45
-
46
- # before responding, looks at the following headers
47
- # if-modified-since: (date from client's cache)
48
- # if-none-match: (etag from client's cache)
49
- # cache-control: no-cache will prevent caching
50
- req.serve_file(path, base_path: STATIC_PATH, cache: true)
51
-
52
- # We can control this manually instead:
53
- req.serve_file(path, base_path: STATIC_PATH, cache: {
54
- etag: 'blahblah',
55
- last_modified: Time.now - 365*86400,
56
- vary: 'Accept-Encoding, User-Agent'
57
- })
15
+ ```ruby
16
+ req.route do
17
+ req.on_host 'example.com' do
18
+ req.redirect "https://www.example.com#{req.uri}"
19
+ end
58
20
  end
59
- end
60
- ```
21
+ ```
61
22
 
62
- So, the algorithm:
23
+ - `#on_http`:
63
24
 
64
- ```ruby
65
- def validate_client_cache(path)
66
- return false if headers['cache-control'] == 'no-cache'
67
-
68
- stat = File.stat(path)
69
- etag = file_stat_to_etag(path, stat)
70
- return false if headers['if-none-match'] != etag
25
+ ```ruby
26
+ req.route do
27
+ req.on_http do
28
+ req.redirect "https://#{req.host}#{req.uri}"
29
+ end
30
+ end
31
+ ```
71
32
 
72
- modified = file_stat_to_modified_stamp(stat)
73
- return false if headers['if-modified-since'] != modified
33
+ - shorthand:
74
34
 
75
- true
76
- end
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
+ ```
77
43
 
78
- def file_stat_to_etag(path, stat)
79
- "#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
80
- end
44
+ ## templates
81
45
 
82
- require 'time'
46
+ - needs to be pluggable - allow any kind of template
83
47
 
84
- def file_stat_to_modified_stamp(stat)
85
- stat.mtime.httpdate
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
86
67
  end
87
68
  ```
88
69
 
89
- ## response compression
70
+ ## Rewriting URLs
90
71
 
91
72
  ```ruby
92
- req.route do
93
- req.on 'assets' do
94
- # gzip on the fly
95
- req.serve_file(path, base_path: STATIC_PATH, gzip: :gzip)
96
- end
97
- end
73
+ app = Tipi.app do |r|
74
+ r.rewrite '/' => '/docs/index.html'
75
+ r.rewrite '/docs' => '/docs/'
76
+ r.rewrite '/docs/' => '/docs/index.html'
98
77
 
78
+ # or maybe
79
+ r.on '/docs/'
80
+ end
99
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Qeweney
3
+ module Qeweney
4
4
  end
5
5
 
6
6
  require_relative 'qeweney/request.rb'
@@ -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
@@ -3,6 +3,7 @@
3
3
  require_relative './request_info'
4
4
  require_relative './routing'
5
5
  require_relative './response'
6
+ require_relative './rack'
6
7
 
7
8
  module Qeweney
8
9
  # HTTP request
@@ -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
@@ -3,6 +3,7 @@
3
3
  require 'time'
4
4
  require 'zlib'
5
5
  require 'stringio'
6
+ require 'digest/sha1'
6
7
 
7
8
  require_relative 'status'
8
9
 
@@ -20,10 +21,44 @@ module Qeweney
20
21
  end
21
22
 
22
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
+
23
48
  def redirect(url, status = Status::FOUND)
24
49
  respond(nil, ':status' => status, 'Location' => url)
25
50
  end
26
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
+
27
62
  def serve_file(path, opts)
28
63
  full_path = file_full_path(path, opts)
29
64
  stat = File.stat(full_path)
@@ -107,5 +142,14 @@ module Qeweney
107
142
  )
108
143
  respond(buf.string, headers)
109
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
153
+ end
110
154
  end
111
155
  end
@@ -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 routing_path
20
- @__routing_path__
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
- if route
27
- regexp = (@@regexp_cache[route] ||= /^\/#{route}(\/.*)?/)
28
- return unless @__routing_path__ =~ regexp
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
- @__routing_path__ = Regexp.last_match(1) || '/'
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 @__routing_path__ == route
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 @__routing_path__ == '/'
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
- yield if block_given?
126
+ throw :stop, :found
127
+ end
128
+
129
+ def default
130
+ yield
91
131
  throw :stop, :found
92
132
  end
93
133
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Qeweney
4
- VERSION = '0.4'
4
+ VERSION = '0.7.2'
5
5
  end
data/qeweney.gemspec CHANGED
@@ -22,4 +22,5 @@ Gem::Specification.new do |s|
22
22
  s.add_development_dependency 'rake', '~>12.3.3'
23
23
  s.add_development_dependency 'minitest', '~>5.11.3'
24
24
  s.add_development_dependency 'minitest-reporters', '~>1.4.2'
25
+ s.add_development_dependency 'benchmark-ips', '~>2.8.3'
25
26
  end
data/test/helper.rb CHANGED
@@ -24,6 +24,8 @@ module Qeweney
24
24
  end
25
25
 
26
26
  def self.mock(headers = {})
27
+ headers[':method'] ||= ''
28
+ headers[':path'] ||= ''
27
29
  Request.new(headers, MockAdapter.new)
28
30
  end
29
31
 
@@ -113,3 +113,73 @@ class StaticFileResponeTest < MiniTest::Test
113
113
  ], r.response_calls
114
114
  end
115
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,116 @@
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
+ r.on('baz') { r.respond(r.route_relative_path) }
88
+ end
89
+
90
+ r = Qeweney.mock(':path' => '/')
91
+ app.(r)
92
+ assert_equal '/', default_relative_path
93
+ assert_equal [[:respond, 'ROOT/', {}]], r.response_calls
94
+
95
+
96
+ r = Qeweney.mock(':path' => '/foo/bar/baz')
97
+ app.(r)
98
+ assert_equal '/foo/bar/baz', default_relative_path
99
+ assert_equal [[:respond, 'FOO/bar/baz', {}]], r.response_calls
100
+
101
+ r = Qeweney.mock(':path' => '/bar/a/b/c')
102
+ app.(r)
103
+ assert_equal '/bar/a/b/c', default_relative_path
104
+ assert_equal [[:respond, 'BAR/a/b/c', {}]], r.response_calls
105
+
106
+ r = Qeweney.mock(':path' => '/bar/baz/b/c')
107
+ app.(r)
108
+ assert_equal '/bar/baz/b/c', default_relative_path
109
+ assert_equal [[:respond, 'BAR/BAZ/b/c', {}]], r.response_calls
110
+
111
+ r = Qeweney.mock(':path' => '/baz/d/e/f')
112
+ app.(r)
113
+ assert_equal '/baz/d/e/f', default_relative_path
114
+ assert_equal [[:respond, '/d/e/f', {}]], r.response_calls
115
+ end
116
+ 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: '0.4'
4
+ version: 0.7.2
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-02-12 00:00:00.000000000 Z
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
@@ -95,6 +111,7 @@ files:
95
111
  - test/run.rb
96
112
  - test/test_request_info.rb
97
113
  - test/test_response.rb
114
+ - test/test_routing.rb
98
115
  homepage: http://github.com/digital-fabric/qeweney
99
116
  licenses:
100
117
  - MIT