qeweney 0.5 → 0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82e2e4adbe7c5c163bb7252c666659baea7758f726a3426f9af837c0d09408ac
4
- data.tar.gz: b7aee9e0c09876fd2730905dc61617945636bee6c3891bae45573f910aa67398
3
+ metadata.gz: dc2a56f6f21001d4122d990b735f952521f2949b6d76811bc2f5e528f60ca8ed
4
+ data.tar.gz: e7f1a6fa8e788ef4bf1612ac043f847497f3c4b9558a8959ad75182806106088
5
5
  SHA512:
6
- metadata.gz: 1225cb063c858b5e3f0e0e465d778771c8a8a4fec55d5b431d93489d637a8768c1787c1ea28aa6417f83d7666504e433bf338e48b483616d3b9165bb8e1f3465
7
- data.tar.gz: 4fececba98bf9f24e0accef005c36eb62fe13bb5955e380b9324de71660c6dd156c1fb077a13051d1855b971a685be712f0e8873752cb414466974a15d8b8b4f
6
+ metadata.gz: 4be0bb1c58d67ccaa8550b6b1ce59dfbc5cdb6a87d19ede17702293eb521bec416d9932b54627eb43ba832c55dee4e4320983d6eb14fdae4131d751eb459391a
7
+ data.tar.gz: 5cf454ddf41665c9abf6cdba864161257c2d11da5c60add1926cc27e9ba5fcef3a3eeba003815e4cbd8095d70632ed999390cfd07912a135cb33813155876762
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.6 2021-03-03
2
+
3
+ - More work on routing (still WIP)
4
+ - Implement two-way Rack adapters
5
+
1
6
  ## 0.5 2021-02-15
2
7
 
3
8
  - Implement upgrade and WebSocket upgrade responses
data/Gemfile.lock CHANGED
@@ -1,13 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- qeweney (0.5)
4
+ qeweney (0.6)
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!
@@ -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
@@ -142,5 +142,14 @@ module Qeweney
142
142
  )
143
143
  respond(buf.string, headers)
144
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
145
154
  end
146
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,41 @@ 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 enter_route
29
+ @path_parts_idx += 1
30
+ end
31
+
32
+ def leave_route
33
+ @path_parts_idx -= 1
34
+ end
29
35
 
30
- @__routing_path__ = Regexp.last_match(1) || '/'
36
+ def on(route = nil, &block)
37
+ if route
38
+ return unless @path_parts[@path_parts_idx] == route
31
39
  end
32
40
 
41
+ enter_route
33
42
  route_found(&block)
43
+ leave_route
34
44
  end
35
45
 
36
46
  def is(route = '/', &block)
37
- return unless @__routing_path__ == route
47
+ return unless @path_parts[@path_parts_idx] == route && @path_parts_idx >= @path_parts.size
38
48
 
39
49
  route_found(&block)
40
50
  end
41
51
 
42
52
  def on_root(&block)
43
- return unless @__routing_path__ == '/'
53
+ return unless @path_parts_idx > @path_parts.size - 1
44
54
 
45
55
  route_found(&block)
46
56
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Qeweney
4
- VERSION = '0.5'
4
+ VERSION = '0.6'
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
 
@@ -166,3 +166,20 @@ class UpgradeTest < MiniTest::Test
166
166
  ], r.response_calls
167
167
  end
168
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,75 @@
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
+ 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.5'
4
+ version: '0.6'
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-15 00:00:00.000000000 Z
11
+ date: 2021-03-03 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