qeweney 0.5 → 0.7.3

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: 8410ab97bb9ce59f14cd87659fa5eeb77595f2a4a1fad873392e403d0b27a80a
4
+ data.tar.gz: 5b8404bf25b0b23cb72f0e3e919245d7f41361b692601f0e1bf5b208bc0215ed
5
5
  SHA512:
6
- metadata.gz: 1225cb063c858b5e3f0e0e465d778771c8a8a4fec55d5b431d93489d637a8768c1787c1ea28aa6417f83d7666504e433bf338e48b483616d3b9165bb8e1f3465
7
- data.tar.gz: 4fececba98bf9f24e0accef005c36eb62fe13bb5955e380b9324de71660c6dd156c1fb077a13051d1855b971a685be712f0e8873752cb414466974a15d8b8b4f
6
+ metadata.gz: 5ec5cb26a660c15cd465c04597c8135857f18b5cf8f706582243e3406f9163038c515d4d7a13678777ad71ab4c6fe26b11b35df8d27673316cf423f844c0529d
7
+ data.tar.gz: 85d6961f2eb42cf6d7299318e24dbf0ac697a89051667be99fc485b1359c0a0365eabeb69bbe2744d70bbefe9bcbce0226a1cf37196480defc7a6845572963e8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.7.3 2021-03-05
2
+
3
+ - Fix `parse_urlencoded_form_data`
4
+
5
+ ## 0.7.2 2021-03-04
6
+
7
+ - Add `#route_relative_path` method
8
+
9
+ ## 0.6 2021-03-03
10
+
11
+ - More work on routing (still WIP)
12
+ - Implement two-way Rack adapters
13
+
1
14
  ## 0.5 2021-02-15
2
15
 
3
16
  - 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.7.3)
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
@@ -58,7 +58,7 @@ PAGES = Tipi::PageManager(
58
58
 
59
59
  app = Tipi.app do |r|
60
60
  r.on 'static' do
61
- r.serve_file(r.routing_path, base_path: ASSETS_PATH)
61
+ r.serve_file(r.route_path, base_path: ASSETS_PATH)
62
62
  end
63
63
 
64
64
  r.default do
@@ -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
@@ -138,7 +138,7 @@ module Qeweney
138
138
  MAX_PARAMETER_VALUE_SIZE = 2**20 # 1MB
139
139
 
140
140
  def parse_urlencoded_form_data(body)
141
- body.force_encoding(UTF_8) unless body.encoding == Encoding::UTF_8
141
+ body.force_encoding(Encoding::UTF_8) unless body.encoding == Encoding::UTF_8
142
142
  body.split('&').each_with_object({}) do |i, m|
143
143
  raise 'Invalid parameter format' unless i =~ PARAMETER_RE
144
144
 
@@ -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,45 @@ 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
29
31
 
30
- @__routing_path__ = Regexp.last_match(1) || '/'
32
+ def enter_route
33
+ @path_parts_idx += 1
34
+ end
35
+
36
+ def leave_route
37
+ @path_parts_idx -= 1
38
+ end
39
+
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
44
58
 
45
59
  route_found(&block)
46
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Qeweney
4
- VERSION = '0.5'
4
+ VERSION = '0.7.3'
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,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.5'
4
+ version: 0.7.3
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-05 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