nuhttp 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +161 -0
- data/Rakefile +8 -0
- data/exe/nuhttp-typegen +116 -0
- data/lib/nuhttp/app.rb +26 -0
- data/lib/nuhttp/builder.rb +57 -0
- data/lib/nuhttp/context.rb +98 -0
- data/lib/nuhttp/herb_renderer.rb +28 -0
- data/lib/nuhttp/rack_port.rb +44 -0
- data/lib/nuhttp/router.rb +56 -0
- data/lib/nuhttp/server.rb +184 -0
- data/lib/nuhttp/version.rb +6 -0
- data/lib/nuhttp.rb +36 -0
- data/sig/generated/nuhttp/app.rbs +13 -0
- data/sig/generated/nuhttp/builder.rbs +12 -0
- data/sig/generated/nuhttp/context.rbs +65 -0
- data/sig/generated/nuhttp/herb_renderer.rbs +11 -0
- data/sig/generated/nuhttp/rack_port.rbs +11 -0
- data/sig/generated/nuhttp/router.rbs +32 -0
- data/sig/generated/nuhttp/server.rbs +17 -0
- data/sig/generated/nuhttp/version.rbs +5 -0
- data/sig/generated/nuhttp.rbs +18 -0
- data/sig/nuhttp.rbs +4 -0
- data/test/app_test.rb +42 -0
- data/test/context_response_test.rb +74 -0
- data/test/herb_renderer_test.rb +35 -0
- data/test/rack_port_test.rb +58 -0
- data/test/router_test.rb +35 -0
- data/typecheck.png +0 -0
- metadata +128 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module NuHttp
|
|
7
|
+
class Server
|
|
8
|
+
# @rbs skip
|
|
9
|
+
include Socket::Constants
|
|
10
|
+
|
|
11
|
+
RFC9110_REASON_PHRASES = {
|
|
12
|
+
100 => "Continue",
|
|
13
|
+
101 => "Switching Protocols",
|
|
14
|
+
200 => "OK",
|
|
15
|
+
201 => "Created",
|
|
16
|
+
202 => "Accepted",
|
|
17
|
+
203 => "Non-Authoritative Information",
|
|
18
|
+
204 => "No Content",
|
|
19
|
+
205 => "Reset Content",
|
|
20
|
+
206 => "Partial Content",
|
|
21
|
+
300 => "Multiple Choices",
|
|
22
|
+
301 => "Moved Permanently",
|
|
23
|
+
302 => "Found",
|
|
24
|
+
303 => "See Other",
|
|
25
|
+
304 => "Not Modified",
|
|
26
|
+
305 => "Use Proxy",
|
|
27
|
+
307 => "Temporary Redirect",
|
|
28
|
+
308 => "Permanent Redirect",
|
|
29
|
+
400 => "Bad Request",
|
|
30
|
+
401 => "Unauthorized",
|
|
31
|
+
402 => "Payment Required",
|
|
32
|
+
403 => "Forbidden",
|
|
33
|
+
404 => "Not Found",
|
|
34
|
+
405 => "Method Not Allowed",
|
|
35
|
+
406 => "Not Acceptable",
|
|
36
|
+
407 => "Proxy Authentication Required",
|
|
37
|
+
408 => "Request Timeout",
|
|
38
|
+
409 => "Conflict",
|
|
39
|
+
410 => "Gone",
|
|
40
|
+
411 => "Length Required",
|
|
41
|
+
412 => "Precondition Failed",
|
|
42
|
+
413 => "Content Too Large",
|
|
43
|
+
414 => "URI Too Long",
|
|
44
|
+
415 => "Unsupported Media Type",
|
|
45
|
+
416 => "Range Not Satisfiable",
|
|
46
|
+
417 => "Expectation Failed",
|
|
47
|
+
421 => "Misdirected Request",
|
|
48
|
+
422 => "Unprocessable Content",
|
|
49
|
+
426 => "Upgrade Required",
|
|
50
|
+
500 => "Internal Server Error",
|
|
51
|
+
501 => "Not Implemented",
|
|
52
|
+
502 => "Bad Gateway",
|
|
53
|
+
503 => "Service Unavailable",
|
|
54
|
+
504 => "Gateway Timeout",
|
|
55
|
+
505 => "HTTP Version Not Supported",
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
def initialize(app, bind: "127.0.0.1", port: 8080, ractor_mode: false, call_make_shareable: false)
|
|
59
|
+
@app = app
|
|
60
|
+
@bind_address = bind
|
|
61
|
+
@port = port
|
|
62
|
+
@ractor_mode = ractor_mode
|
|
63
|
+
|
|
64
|
+
# When app_make_shareable: true, app will be deep frozen in attempt to
|
|
65
|
+
# make it Ractor shareable.
|
|
66
|
+
# Note: the app may not be made shareable, even if this option is specified.
|
|
67
|
+
Ractor.make_shareable(@app) if call_make_shareable
|
|
68
|
+
raise "app is not shareable" if @ractor_mode && !Ractor.shareable?(@app)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def start
|
|
72
|
+
socket = Socket.new(AF_INET, SOCK_STREAM, 0)
|
|
73
|
+
socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, true)
|
|
74
|
+
sockaddr = Socket.pack_sockaddr_in(@port, @bind_address)
|
|
75
|
+
socket.bind(sockaddr)
|
|
76
|
+
socket.listen(64) # backlog
|
|
77
|
+
puts "Listening on #{@bind_address}:#{@port}"
|
|
78
|
+
|
|
79
|
+
loop do
|
|
80
|
+
connn, _ = socket.accept # chose awkward name to avoid shadowing
|
|
81
|
+
|
|
82
|
+
if @ractor_mode
|
|
83
|
+
# Make sure @app does not get copied on Ractor.new
|
|
84
|
+
Ractor.new(@app) do |app|
|
|
85
|
+
conn = Ractor.receive
|
|
86
|
+
Server.handle(app, conn)
|
|
87
|
+
nil # reduce implicit copy on #value
|
|
88
|
+
ensure
|
|
89
|
+
conn.close
|
|
90
|
+
end.send(connn, move: true)
|
|
91
|
+
else
|
|
92
|
+
Server.handle(@app, connn)
|
|
93
|
+
connn.close
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
ensure
|
|
97
|
+
socket.close if defined?(socket)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.handle(app, conn)
|
|
101
|
+
request = HttpParser.parse_request(conn)
|
|
102
|
+
|
|
103
|
+
begin
|
|
104
|
+
res = app.dispatch(request)
|
|
105
|
+
rescue => e
|
|
106
|
+
error_message = [
|
|
107
|
+
"#{e.class}: #{e.message}",
|
|
108
|
+
e.backtrace&.map { |line| "\tfrom #{line}" }&.join("\n")
|
|
109
|
+
].join("\n")
|
|
110
|
+
puts error_message
|
|
111
|
+
|
|
112
|
+
res = Response.new.tap do |r|
|
|
113
|
+
r.status = 500
|
|
114
|
+
r.headers["Content-Type"] = "text/plain"
|
|
115
|
+
r.body = "Internal Server Error\n"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
reason_phrase = RFC9110_REASON_PHRASES[res.status] || "Unknown"
|
|
120
|
+
|
|
121
|
+
conn.write "HTTP/1.1 #{res.status} #{reason_phrase}\r\n"
|
|
122
|
+
|
|
123
|
+
# Send headers
|
|
124
|
+
res.headers.each do |header_name, header_body|
|
|
125
|
+
conn.write "#{header_name}: #{header_body}\r\n"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
chunked = res.headers["Transfer-Encoding"] == "chunked" # ?
|
|
129
|
+
|
|
130
|
+
# Send body
|
|
131
|
+
body = res.body
|
|
132
|
+
if !chunked
|
|
133
|
+
body_str = String.new
|
|
134
|
+
body_str << body
|
|
135
|
+
|
|
136
|
+
conn.write "Content-Length: #{body_str.bytesize}\r\n"
|
|
137
|
+
conn.write "Connection: Close\r\n" # no keepalive impl (yet)
|
|
138
|
+
conn.write "\r\n"
|
|
139
|
+
|
|
140
|
+
conn.write body_str
|
|
141
|
+
else
|
|
142
|
+
conn.write "\r\n"
|
|
143
|
+
|
|
144
|
+
body.each do |chunk|
|
|
145
|
+
conn.write chunk.bytesize.to_s(16) + "\r\n"
|
|
146
|
+
conn.write chunk
|
|
147
|
+
conn.write "\r\n"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
conn.write "0" + "\r\n\r\n"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
module HttpParser
|
|
155
|
+
class << self
|
|
156
|
+
def parse_request(io)
|
|
157
|
+
request_line = io.gets("\r\n").chomp
|
|
158
|
+
method, request_target, _http_version = request_line.split(' ', 3)
|
|
159
|
+
|
|
160
|
+
headers = {}
|
|
161
|
+
while line = io.gets("\r\n").chomp
|
|
162
|
+
break if line.empty?
|
|
163
|
+
|
|
164
|
+
header_name, header_value = line.split(':', 2)
|
|
165
|
+
headers[header_name] = header_value.strip
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
path, query = request_target.split('?')
|
|
169
|
+
query ||= ''
|
|
170
|
+
|
|
171
|
+
body =
|
|
172
|
+
if length = headers['Content-Length'] # TODO: match any case
|
|
173
|
+
io.read(length.to_i)
|
|
174
|
+
else
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
Request.new(method:, path:, query:, headers:, body:)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
data/lib/nuhttp.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative 'nuhttp/app'
|
|
5
|
+
require_relative 'nuhttp/builder'
|
|
6
|
+
require_relative 'nuhttp/context'
|
|
7
|
+
require_relative 'nuhttp/router'
|
|
8
|
+
require_relative 'nuhttp/version'
|
|
9
|
+
|
|
10
|
+
module NuHttp
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Entrypoint to define a NuHttp app.
|
|
14
|
+
# If a block is given, a NuHttp::Builder is yielded.
|
|
15
|
+
# If not, a bare builder will be returned.
|
|
16
|
+
# @rbs &block: (Builder) -> void
|
|
17
|
+
def self.app(&block)
|
|
18
|
+
builder = Builder.new
|
|
19
|
+
return builder if block == nil
|
|
20
|
+
block.call(builder)
|
|
21
|
+
builder.build
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Entrypoint to define a Ractor-compatible NuHttp app.
|
|
25
|
+
# Note that many features will be restricted in Ractor mode.
|
|
26
|
+
# See Ruby documentation for details on Ractor limitations.
|
|
27
|
+
# @rbs &block: (Builder) -> void
|
|
28
|
+
def self.ractor_app(&block)
|
|
29
|
+
# Ractor-compatible apps must be frozen at the time of creation
|
|
30
|
+
raise ArgumentError if block == nil
|
|
31
|
+
|
|
32
|
+
builder = Builder.new(ractor_compat: true)
|
|
33
|
+
block.call(builder)
|
|
34
|
+
builder.build
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Generated from lib/nuhttp/app.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module NuHttp
|
|
4
|
+
class App
|
|
5
|
+
def initialize: (untyped router) -> untyped
|
|
6
|
+
|
|
7
|
+
# The entrypoint of the request.
|
|
8
|
+
# @rbs (NuHttp::Context[untyped]) -> void
|
|
9
|
+
def dispatch: (NuHttp::Context[untyped]) -> void
|
|
10
|
+
|
|
11
|
+
def routes: () -> untyped
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Generated from lib/nuhttp/builder.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module NuHttp
|
|
4
|
+
class Builder
|
|
5
|
+
def initialize: (?ractor_compat: untyped) -> untyped
|
|
6
|
+
|
|
7
|
+
def build: () -> untyped
|
|
8
|
+
|
|
9
|
+
# : (String, NuHttp::App) -> void
|
|
10
|
+
def mount: (String, NuHttp::App) -> void
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Generated from lib/nuhttp/context.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module NuHttp
|
|
4
|
+
# @rbs generic ParamsT
|
|
5
|
+
class Context[ParamsT]
|
|
6
|
+
def initialize: (req: untyped, route: untyped) -> untyped
|
|
7
|
+
|
|
8
|
+
def req: () -> Request[ParamsT]
|
|
9
|
+
|
|
10
|
+
def res: () -> Response
|
|
11
|
+
|
|
12
|
+
def status: (untyped code) -> untyped
|
|
13
|
+
|
|
14
|
+
def header: (untyped name, untyped value) -> untyped
|
|
15
|
+
|
|
16
|
+
# TODO: take IO and allow streaming
|
|
17
|
+
def body: (untyped str) -> untyped
|
|
18
|
+
|
|
19
|
+
# Set response to HTML `str`.
|
|
20
|
+
# Content-Type header will be set to `text/html; charset=utf-8`.
|
|
21
|
+
# : (String) -> void
|
|
22
|
+
def html: (String) -> void
|
|
23
|
+
|
|
24
|
+
# Render a template using the provided renderer and set as HTML response.
|
|
25
|
+
# : (String, ?Hash[Symbol, untyped]) -> void
|
|
26
|
+
def erb: (String, ?Hash[Symbol, untyped]) -> void
|
|
27
|
+
|
|
28
|
+
# Set response to plain text `str`.
|
|
29
|
+
# Content-Type header will be set to `text/plain; charset=utf-8`.
|
|
30
|
+
# : (String) -> void
|
|
31
|
+
def text: (String) -> void
|
|
32
|
+
|
|
33
|
+
# Set response to the JSON representation of `obj`.
|
|
34
|
+
# Content-Type header will be set to `application/json`.
|
|
35
|
+
# : (Object) -> void
|
|
36
|
+
def json: (Object) -> void
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @rbs generic ParamsT
|
|
40
|
+
class Request[ParamsT]
|
|
41
|
+
attr_reader method: untyped
|
|
42
|
+
|
|
43
|
+
attr_reader path: untyped
|
|
44
|
+
|
|
45
|
+
attr_reader query: untyped
|
|
46
|
+
|
|
47
|
+
attr_reader headers: untyped
|
|
48
|
+
|
|
49
|
+
attr_reader body: untyped
|
|
50
|
+
|
|
51
|
+
attr_accessor params: ParamsT
|
|
52
|
+
|
|
53
|
+
def initialize: (method: untyped, path: untyped, query: untyped, body: untyped, ?headers: untyped) -> untyped
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Response
|
|
57
|
+
attr_accessor status: untyped
|
|
58
|
+
|
|
59
|
+
attr_accessor headers: untyped
|
|
60
|
+
|
|
61
|
+
attr_accessor body: untyped
|
|
62
|
+
|
|
63
|
+
def initialize: () -> untyped
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Generated from lib/nuhttp/router.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module NuHttp
|
|
4
|
+
class Router
|
|
5
|
+
class Route < Data
|
|
6
|
+
attr_reader method(): untyped
|
|
7
|
+
|
|
8
|
+
attr_reader pattern(): untyped
|
|
9
|
+
|
|
10
|
+
attr_reader handler(): untyped
|
|
11
|
+
|
|
12
|
+
def self.new: (untyped method, untyped pattern, untyped handler) -> instance
|
|
13
|
+
| (method: untyped, pattern: untyped, handler: untyped) -> instance
|
|
14
|
+
|
|
15
|
+
def self.members: () -> [ :method, :pattern, :handler ]
|
|
16
|
+
|
|
17
|
+
def members: () -> [ :method, :pattern, :handler ]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
NOT_FOUND_ROUTE: untyped
|
|
21
|
+
|
|
22
|
+
def initialize: () -> untyped
|
|
23
|
+
|
|
24
|
+
def routes: () -> untyped
|
|
25
|
+
|
|
26
|
+
# @rbs (Symbol, String) -> void
|
|
27
|
+
def register_route: (Symbol, String) -> void
|
|
28
|
+
|
|
29
|
+
# @rbs (NuHttp::Request[untyped]) -> [Route, Hash[untyped, untyped]]
|
|
30
|
+
def resolve: (NuHttp::Request[untyped]) -> [ Route, Hash[untyped, untyped] ]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated from lib/nuhttp/server.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module NuHttp
|
|
4
|
+
class Server
|
|
5
|
+
RFC9110_REASON_PHRASES: untyped
|
|
6
|
+
|
|
7
|
+
def initialize: (untyped app, ?bind: untyped, ?port: untyped, ?ractor_mode: untyped, ?call_make_shareable: untyped) -> untyped
|
|
8
|
+
|
|
9
|
+
def start: () -> untyped
|
|
10
|
+
|
|
11
|
+
def self.handle: (untyped app, untyped conn) -> untyped
|
|
12
|
+
|
|
13
|
+
module HttpParser
|
|
14
|
+
def self.parse_request: (untyped io) -> untyped
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated from lib/nuhttp.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module NuHttp
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# Entrypoint to define a NuHttp app.
|
|
8
|
+
# If a block is given, a NuHttp::Builder is yielded.
|
|
9
|
+
# If not, a bare builder will be returned.
|
|
10
|
+
# @rbs &block: (Builder) -> void
|
|
11
|
+
def self.app: () { (Builder) -> void } -> untyped
|
|
12
|
+
|
|
13
|
+
# Entrypoint to define a Ractor-compatible NuHttp app.
|
|
14
|
+
# Note that many features will be restricted in Ractor mode.
|
|
15
|
+
# See Ruby documentation for details on Ractor limitations.
|
|
16
|
+
# @rbs &block: (Builder) -> void
|
|
17
|
+
def self.ractor_app: () { (Builder) -> void } -> untyped
|
|
18
|
+
end
|
data/sig/nuhttp.rbs
ADDED
data/test/app_test.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'nuhttp'
|
|
5
|
+
|
|
6
|
+
class AppTest < Minitest::Test
|
|
7
|
+
def test_basic_test
|
|
8
|
+
app = NuHttp.app do |b|
|
|
9
|
+
b.get '/' do |c|
|
|
10
|
+
c.res.body = "Hello, World!\n"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
15
|
+
res = app.dispatch(req)
|
|
16
|
+
assert_equal "Hello, World!\n", res.body
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_subapp_mounting
|
|
20
|
+
subapp = NuHttp.app do |b|
|
|
21
|
+
b.get '/' do |c|
|
|
22
|
+
c.text "subapp root"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
b.get '/details' do |c|
|
|
26
|
+
c.text "subapp details"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
app = NuHttp.app do |b|
|
|
31
|
+
b.mount '/subapp', subapp
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
req = NuHttp::Request.new(method: :get, path: '/subapp/', query: '', body: nil)
|
|
35
|
+
res = app.dispatch(req)
|
|
36
|
+
assert_equal "subapp root", res.body
|
|
37
|
+
|
|
38
|
+
req = NuHttp::Request.new(method: :get, path: '/subapp/details', query: '', body: nil)
|
|
39
|
+
res = app.dispatch(req)
|
|
40
|
+
assert_equal "subapp details", res.body
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'nuhttp'
|
|
5
|
+
|
|
6
|
+
class ContextResponseTest < Minitest::Test
|
|
7
|
+
def test_status
|
|
8
|
+
app = NuHttp.app do |b|
|
|
9
|
+
b.get '/' do |c|
|
|
10
|
+
c.status(201)
|
|
11
|
+
c.text("Created")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
16
|
+
res = app.dispatch(req)
|
|
17
|
+
assert_equal 201, res.status
|
|
18
|
+
assert_equal "Created", res.body
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_header
|
|
22
|
+
app = NuHttp.app do |b|
|
|
23
|
+
b.get '/' do |c|
|
|
24
|
+
c.status(302)
|
|
25
|
+
c.header('Location', 'https://example.com')
|
|
26
|
+
c.text("Redirecting")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
31
|
+
res = app.dispatch(req)
|
|
32
|
+
assert_equal 302, res.status
|
|
33
|
+
assert_equal 'https://example.com', res.headers['Location']
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_html_helper
|
|
37
|
+
app = NuHttp.app do |b|
|
|
38
|
+
b.get '/' do |c|
|
|
39
|
+
c.html("<!DOCTYPE html><html><body><h1>hello, world</h1></body></html>")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
44
|
+
res = app.dispatch(req)
|
|
45
|
+
assert_equal "text/html; charset=utf-8", res.headers['Content-Type']
|
|
46
|
+
assert_equal "<!DOCTYPE html><html><body><h1>hello, world</h1></body></html>", res.body
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_text_helper
|
|
50
|
+
app = NuHttp.app do |b|
|
|
51
|
+
b.get '/' do |c|
|
|
52
|
+
c.text("hello, world")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
57
|
+
res = app.dispatch(req)
|
|
58
|
+
assert_equal "text/plain; charset=utf-8", res.headers['Content-Type']
|
|
59
|
+
assert_equal "hello, world", res.body
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_json_helper
|
|
63
|
+
app = NuHttp.app do |b|
|
|
64
|
+
b.get '/' do |c|
|
|
65
|
+
c.json({hello: "world"})
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
70
|
+
res = app.dispatch(req)
|
|
71
|
+
assert_equal "application/json", res.headers['Content-Type']
|
|
72
|
+
assert_equal '{"hello":"world"}', res.body
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'nuhttp'
|
|
5
|
+
require 'nuhttp/herb_renderer'
|
|
6
|
+
|
|
7
|
+
class HerbRendererTest < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
@htmlerb_file = Tempfile.open("greeting.html.erb")
|
|
10
|
+
@htmlerb_file.tap {|f| f.write("<p>hello, <%= name %></p>\n"); f.flush }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
@htmlerb_file.close!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_renders_template
|
|
18
|
+
html = NuHttp::HerbRenderer.new.render(@htmlerb_file.path, name: "world")
|
|
19
|
+
assert_equal "<p>hello, world</p>\n", html
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_context_render_sets_html_response
|
|
23
|
+
app = NuHttp.app do |b|
|
|
24
|
+
b.get '/' do |c|
|
|
25
|
+
c.erb(@htmlerb_file.path, { name: "world" })
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
30
|
+
res = app.dispatch(req)
|
|
31
|
+
|
|
32
|
+
assert_equal "text/html; charset=utf-8", res.headers['Content-Type']
|
|
33
|
+
assert_equal "<p>hello, world</p>\n", res.body
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'nuhttp'
|
|
5
|
+
require 'nuhttp/rack_port'
|
|
6
|
+
|
|
7
|
+
class RackPortTest < Minitest::Test
|
|
8
|
+
def test_env_to_request_bare_request
|
|
9
|
+
env = {
|
|
10
|
+
'REQUEST_METHOD' => 'GET',
|
|
11
|
+
'PATH_INFO' => '/',
|
|
12
|
+
'QUERY_STRING' => ''
|
|
13
|
+
}
|
|
14
|
+
req = NuHttp::RackPort.new(env).send(:env_to_request, env)
|
|
15
|
+
|
|
16
|
+
assert_equal 'GET', req.method
|
|
17
|
+
assert_equal '/', req.path
|
|
18
|
+
assert_equal '', req.query
|
|
19
|
+
assert_equal({}, req.headers)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_env_to_request_with_headers
|
|
23
|
+
env = {
|
|
24
|
+
'REQUEST_METHOD' => 'GET',
|
|
25
|
+
'PATH_INFO' => '/',
|
|
26
|
+
'QUERY_STRING' => '',
|
|
27
|
+
'CONTENT_LENGTH' => '42',
|
|
28
|
+
'CONTENT_TYPE' => 'text/plain',
|
|
29
|
+
'HTTP_HOST' => 'example.com',
|
|
30
|
+
'HTTP_X_CUSTOM_HEADER' => 'CustomValue'
|
|
31
|
+
}
|
|
32
|
+
req = NuHttp::RackPort.new(env).send(:env_to_request, env)
|
|
33
|
+
|
|
34
|
+
assert_equal 'GET', req.method
|
|
35
|
+
assert_equal '/', req.path
|
|
36
|
+
assert_equal '', req.query
|
|
37
|
+
assert_equal({
|
|
38
|
+
'Content-Length' => 42,
|
|
39
|
+
'Content-Type' => 'text/plain',
|
|
40
|
+
'Host' => 'example.com',
|
|
41
|
+
'X-Custom-Header' => 'CustomValue'
|
|
42
|
+
}, req.headers)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_env_to_request_with_body
|
|
46
|
+
env = {
|
|
47
|
+
'REQUEST_METHOD' => 'GET',
|
|
48
|
+
'PATH_INFO' => '/',
|
|
49
|
+
'QUERY_STRING' => ''
|
|
50
|
+
}
|
|
51
|
+
req = NuHttp::RackPort.new(env).send(:env_to_request, env)
|
|
52
|
+
|
|
53
|
+
assert_equal 'GET', req.method
|
|
54
|
+
assert_equal '/', req.path
|
|
55
|
+
assert_equal '', req.query
|
|
56
|
+
assert_equal({}, req.headers)
|
|
57
|
+
end
|
|
58
|
+
end
|
data/test/router_test.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'nuhttp'
|
|
5
|
+
|
|
6
|
+
class RouterTest < Minitest::Test
|
|
7
|
+
def test_root_path_routing
|
|
8
|
+
router = NuHttp::Router.new
|
|
9
|
+
router.register_route(:get, '/')
|
|
10
|
+
|
|
11
|
+
req = NuHttp::Request.new(method: :get, path: '/', query: '', body: nil)
|
|
12
|
+
resolved = router.resolve(req)[0]
|
|
13
|
+
assert_equal :get, resolved.method
|
|
14
|
+
assert_equal '/', resolved.pattern
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_toplevel_path_routing
|
|
18
|
+
router = NuHttp::Router.new
|
|
19
|
+
router.register_route(:get, '/top')
|
|
20
|
+
|
|
21
|
+
req = NuHttp::Request.new(method: :get, path: '/top', query: '', body: nil)
|
|
22
|
+
resolved = router.resolve(req)[0]
|
|
23
|
+
assert_equal :get, resolved.method
|
|
24
|
+
assert_equal '/top', resolved.pattern
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_notfound_path_routing
|
|
28
|
+
router = NuHttp::Router.new
|
|
29
|
+
|
|
30
|
+
req = NuHttp::Request.new(method: :get, path: '/nonexistent', query: '', body: nil)
|
|
31
|
+
resolved = router.resolve(req)[0]
|
|
32
|
+
assert_equal :internal, resolved.method
|
|
33
|
+
assert_nil resolved.pattern
|
|
34
|
+
end
|
|
35
|
+
end
|
data/typecheck.png
ADDED
|
Binary file
|