tynn 2.0.0.alpha → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +168 -29
- data/lib/tynn.rb +0 -4
- data/lib/tynn/base.rb +368 -24
- data/lib/tynn/errors.rb +7 -0
- data/lib/tynn/json.rb +18 -5
- data/lib/tynn/render.rb +36 -25
- data/lib/tynn/request.rb +86 -15
- data/lib/tynn/response.rb +214 -12
- data/lib/tynn/secure_headers.rb +2 -6
- data/lib/tynn/session.rb +14 -5
- data/lib/tynn/ssl.rb +19 -16
- data/lib/tynn/test.rb +45 -38
- data/lib/tynn/utils.rb +15 -0
- data/lib/tynn/version.rb +1 -1
- data/test/default_headers_test.rb +1 -1
- data/test/environment_test.rb +1 -1
- data/test/helper.rb +9 -0
- data/test/json_test.rb +21 -6
- data/test/middleware_test.rb +23 -13
- data/test/plugin_test.rb +1 -1
- data/test/render_test.rb +24 -15
- data/test/request_headers_test.rb +8 -4
- data/test/request_test.rb +9 -0
- data/test/response_test.rb +217 -0
- data/test/routing_test.rb +128 -38
- data/test/secure_headers_test.rb +1 -1
- data/test/session_test.rb +6 -6
- data/test/settings_test.rb +3 -3
- data/test/ssl_test.rb +3 -3
- data/test/static_test.rb +1 -1
- metadata +14 -24
- data/lib/tynn/default_headers.rb +0 -50
- data/lib/tynn/settings.rb +0 -107
data/lib/tynn/errors.rb
ADDED
data/lib/tynn/json.rb
CHANGED
@@ -11,25 +11,38 @@ class Tynn
|
|
11
11
|
# Tynn.plugin(Tynn::JSON)
|
12
12
|
#
|
13
13
|
module JSON
|
14
|
+
def self.setup(app, options = {}) # :nodoc:
|
15
|
+
app.set(:json, {
|
16
|
+
content_type: "application/json"
|
17
|
+
}.merge(options))
|
18
|
+
end
|
19
|
+
|
14
20
|
module InstanceMethods
|
15
|
-
# Generates a JSON document from <tt>data</tt> and writes it to the
|
16
|
-
# It automatically sets the <tt>Content-Type</tt> header
|
21
|
+
# Generates a JSON document from <tt>data</tt> and writes it to the
|
22
|
+
# response body. It automatically sets the <tt>Content-Type</tt> header
|
23
|
+
# to <tt>application/json</tt>.
|
17
24
|
#
|
18
25
|
# Tynn.define do
|
19
|
-
# on
|
26
|
+
# on "hash" do
|
20
27
|
# json(foo: "bar")
|
21
28
|
# end
|
22
29
|
#
|
23
|
-
# on
|
30
|
+
# on "array" do
|
24
31
|
# json([1, 2, 3])
|
25
32
|
# end
|
26
33
|
# end
|
27
34
|
#
|
28
35
|
def json(data)
|
29
|
-
res.
|
36
|
+
res.content_type = json_opts[:content_type]
|
30
37
|
|
31
38
|
res.write(::JSON.generate(data))
|
32
39
|
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def json_opts
|
44
|
+
self.class.settings[:json]
|
45
|
+
end
|
33
46
|
end
|
34
47
|
end
|
35
48
|
end
|
data/lib/tynn/render.rb
CHANGED
@@ -1,52 +1,63 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "hmote"
|
4
4
|
|
5
5
|
class Tynn
|
6
6
|
module Render
|
7
7
|
def self.setup(app, options = {}) # :nodoc:
|
8
8
|
app.set(:render, {
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
}.merge(options.fetch(:options, {}))
|
15
|
-
})
|
9
|
+
content_type: "text/html",
|
10
|
+
layout: "layout",
|
11
|
+
root: Dir.pwd,
|
12
|
+
views: "views"
|
13
|
+
}.merge(options))
|
16
14
|
end
|
17
15
|
|
18
16
|
module InstanceMethods
|
19
|
-
|
20
|
-
res.headers[Rack::CONTENT_TYPE] ||= Syro::Response::DEFAULT
|
17
|
+
include ::HMote::Helpers
|
21
18
|
|
22
|
-
|
19
|
+
# Renders <tt>template</tt> within the default layout. An optional hash of
|
20
|
+
# local variables can be passed to make available inside the template. It
|
21
|
+
# automatically sets the <tt>Content-Type</tt> header to <tt>"text/html"</tt>.
|
22
|
+
#
|
23
|
+
# render("about", title: "About", name: "John Doe")
|
24
|
+
#
|
25
|
+
def render(template, locals = {})
|
26
|
+
res.content_type = render_opts[:content_type]
|
27
|
+
|
28
|
+
res.write(view(template, locals))
|
23
29
|
end
|
24
30
|
|
25
|
-
|
26
|
-
|
31
|
+
# Renders <tt>template</tt> within the default layout. An optional hash of
|
32
|
+
# local variables can be passed to make available inside the template.
|
33
|
+
#
|
34
|
+
# res.write(view("about", title: "About", name: "John Doe"))
|
35
|
+
#
|
36
|
+
def view(template, locals = {})
|
37
|
+
partial(render_opts[:layout], locals.merge(content: partial(template, locals)))
|
27
38
|
end
|
28
39
|
|
40
|
+
# Renders <tt>template</tt> without a layout. An optional hash of local
|
41
|
+
# variables can be passed to make available inside the template.
|
42
|
+
#
|
43
|
+
# res.write(partial("about", name: "John Doe"))
|
44
|
+
#
|
29
45
|
def partial(template, locals = {})
|
30
|
-
|
46
|
+
hmote(template_path(template), locals.merge(app: self), TOPLEVEL_BINDING)
|
31
47
|
end
|
32
48
|
|
33
49
|
private
|
34
50
|
|
35
|
-
def
|
36
|
-
|
37
|
-
Tilt.new(file, 1, opts)
|
38
|
-
}.render(self, locals.merge(app: self))
|
51
|
+
def template_path(template)
|
52
|
+
File.join(views_path, "#{ template }.mote")
|
39
53
|
end
|
40
54
|
|
41
|
-
def
|
42
|
-
|
55
|
+
def views_path
|
56
|
+
File.expand_path(render_opts[:views], render_opts[:root])
|
43
57
|
end
|
44
58
|
|
45
|
-
def
|
46
|
-
|
47
|
-
ext = self.class.settings.dig(:render, :engine)
|
48
|
-
|
49
|
-
File.join(dir, "#{ template }.#{ ext }")
|
59
|
+
def render_opts
|
60
|
+
self.class.settings[:render]
|
50
61
|
end
|
51
62
|
end
|
52
63
|
end
|
data/lib/tynn/request.rb
CHANGED
@@ -5,18 +5,55 @@ class Tynn
|
|
5
5
|
#
|
6
6
|
# env = {
|
7
7
|
# "REQUEST_METHOD" => "GET",
|
8
|
-
# "QUERY_STRING" => "
|
8
|
+
# "QUERY_STRING" => "q=great",
|
9
|
+
# # ...
|
9
10
|
# }
|
10
11
|
#
|
11
12
|
# req = Tynn::Request.new(env)
|
12
13
|
#
|
13
|
-
# req.get?
|
14
|
-
# req.
|
15
|
-
# req.params
|
14
|
+
# req.get? # => true
|
15
|
+
# req.path # => "/search"
|
16
|
+
# req.params # => { "q" => "great" }
|
16
17
|
#
|
17
18
|
class Request < Rack::Request
|
19
|
+
# Returns the content length of the request as an integer.
|
20
|
+
#
|
21
|
+
# req.headers["content-length"]
|
22
|
+
# # => "20"
|
23
|
+
#
|
24
|
+
# req.content_length
|
25
|
+
# # => 20
|
26
|
+
#
|
27
|
+
def content_length
|
28
|
+
super.to_i
|
29
|
+
end
|
30
|
+
|
31
|
+
# Provides access to the request's HTTP headers.
|
32
|
+
#
|
33
|
+
# req.headers["content-type"] # => "application/json"
|
34
|
+
# req.headers.fetch("host") # => "example.org"
|
35
|
+
# req.headers.key?("https") # => true
|
36
|
+
#
|
37
|
+
def headers
|
38
|
+
@headers ||= Headers.new(env)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Provides a uniform way to access HTTP headers from the request.
|
42
|
+
#
|
43
|
+
# headers = Tynn::Request::Headers.new(
|
44
|
+
# "CONTENT_TYPE" => "appplication/json"
|
45
|
+
# "HTTP_HOST" => "example.org"
|
46
|
+
# "HTTPS" => "on"
|
47
|
+
# )
|
48
|
+
#
|
49
|
+
# headers["content-type"] # => "application/json"
|
50
|
+
# headers.fetch("host") # => "example.org"
|
51
|
+
# headers.key?("https") # => true
|
52
|
+
#
|
53
|
+
# This helper class is used by Tynn::Request#headers.
|
54
|
+
#
|
18
55
|
class Headers
|
19
|
-
CGI_VARIABLES =
|
56
|
+
CGI_VARIABLES = %w(
|
20
57
|
AUTH_TYPE
|
21
58
|
CONTENT_LENGTH
|
22
59
|
CONTENT_TYPE
|
@@ -35,22 +72,60 @@ class Tynn
|
|
35
72
|
SERVER_PORT
|
36
73
|
SERVER_PROTOCOL
|
37
74
|
SERVER_SOFTWARE
|
38
|
-
)
|
75
|
+
).freeze # :nodoc:
|
39
76
|
|
40
|
-
def initialize(
|
41
|
-
@
|
77
|
+
def initialize(env) # :nodoc:
|
78
|
+
@env = env
|
42
79
|
end
|
43
80
|
|
81
|
+
# Returns the value for the given <tt>key</tt> mapped
|
82
|
+
# to the request environment (<tt>req.env</tt>).
|
83
|
+
#
|
84
|
+
# req.headers["content-type"]
|
85
|
+
# # => "application/json"
|
86
|
+
#
|
87
|
+
# req.headers["host"]
|
88
|
+
# # => "127.0.0.1"
|
89
|
+
#
|
44
90
|
def [](key)
|
45
|
-
@
|
91
|
+
@env[transform_key(key)]
|
46
92
|
end
|
47
93
|
|
94
|
+
# Returns the value for the given <tt>key</tt> mapped
|
95
|
+
# to the request environment. If the key is not found
|
96
|
+
# and a optional argument or code block is not provided,
|
97
|
+
# raises a <tt>KeyError</tt> exception. If an optional
|
98
|
+
# argument is provided, then it returns its value. If
|
99
|
+
# a code block is provided, then it will be run and its
|
100
|
+
# result returned.
|
101
|
+
#
|
102
|
+
# req.headers.fetch("content-type")
|
103
|
+
# # => "application/json"
|
104
|
+
#
|
105
|
+
# req.headers.fetch("https")
|
106
|
+
# # => KeyError: key not found: "HTTPS"
|
107
|
+
#
|
108
|
+
# req.headers.fetch("https", "")
|
109
|
+
# # => ""
|
110
|
+
#
|
111
|
+
# req.headers.fetch("https") { req.headers["x_forwarded_ssl"] }
|
112
|
+
# # => "on"
|
113
|
+
#
|
48
114
|
def fetch(key, *args, &block)
|
49
|
-
@
|
115
|
+
@env.fetch(transform_key(key), *args, &block)
|
50
116
|
end
|
51
117
|
|
118
|
+
# Returns <tt>true</tt> if the given <tt>key</tt> exists in the
|
119
|
+
# request environment. Otherwise, returns <tt>false</tt>.
|
120
|
+
#
|
121
|
+
# req.headers.key?("content-type")
|
122
|
+
# # => true
|
123
|
+
#
|
124
|
+
# req.headers.key?("https")
|
125
|
+
# # => false
|
126
|
+
#
|
52
127
|
def key?(key)
|
53
|
-
@
|
128
|
+
@env.key?(transform_key(key))
|
54
129
|
end
|
55
130
|
|
56
131
|
private
|
@@ -61,9 +136,5 @@ class Tynn
|
|
61
136
|
key
|
62
137
|
end
|
63
138
|
end
|
64
|
-
|
65
|
-
def headers
|
66
|
-
@headers ||= Headers.new(self)
|
67
|
-
end
|
68
139
|
end
|
69
140
|
end
|
data/lib/tynn/response.rb
CHANGED
@@ -6,20 +6,220 @@ class Tynn
|
|
6
6
|
# res = Tynn::Response.new
|
7
7
|
#
|
8
8
|
# res.status = 200
|
9
|
-
# res["Content-Type"] = "text/
|
10
|
-
# res.write("
|
9
|
+
# res.headers["Content-Type"] = "text/plain"
|
10
|
+
# res.write("hei!")
|
11
11
|
#
|
12
12
|
# res.finish
|
13
|
-
# # => [200, { "Content-Type" => "text/
|
13
|
+
# # => [200, { "Content-Type" => "text/plain", "Content-Length" => 4 }, ["hei!"]]
|
14
14
|
#
|
15
|
-
class Response
|
16
|
-
#
|
15
|
+
class Response
|
16
|
+
# Initializes a new response object. An optional hash of HTTP headers can be
|
17
|
+
# passed.
|
18
|
+
#
|
19
|
+
# res = Tynn::Response.new("Content-Type" => "text/plain")
|
20
|
+
#
|
21
|
+
# res.write("hei!")
|
22
|
+
#
|
23
|
+
# res.finish
|
24
|
+
# # => [200, { "Content-Type" => "text/plain", "Content-Length" => "4" }, ["hei!"]]
|
25
|
+
#
|
26
|
+
def initialize(headers = {})
|
27
|
+
@status = nil
|
28
|
+
@headers = headers
|
29
|
+
@body = []
|
30
|
+
@length = 0
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the status of the response.
|
34
|
+
#
|
35
|
+
# res.status # => 200
|
36
|
+
#
|
37
|
+
def status
|
38
|
+
@status
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets the status of the response.
|
42
|
+
#
|
43
|
+
# res.status = 200
|
44
|
+
#
|
45
|
+
def status=(status)
|
46
|
+
@status = status.to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a hash with the response headers.
|
50
|
+
#
|
51
|
+
# res.headers
|
52
|
+
# # => { "Content-Type" => "text/html", "Content-Length" => "42" }
|
53
|
+
#
|
54
|
+
def headers
|
55
|
+
@headers
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the body of the response.
|
59
|
+
#
|
60
|
+
# res.body
|
61
|
+
# # => []
|
62
|
+
#
|
63
|
+
# res.write("there is")
|
64
|
+
# res.write("no try")
|
65
|
+
#
|
66
|
+
# res.body
|
67
|
+
# # => ["there is", "no try"]
|
68
|
+
#
|
69
|
+
def body
|
70
|
+
@body
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns response <tt>Content-Length</tt> header as an integer. If it
|
74
|
+
# is not present, it returns <tt>nil</tt>.
|
75
|
+
#
|
76
|
+
# res.headers["Content-Length"] # => "42"
|
77
|
+
# res.content_length # => 42
|
78
|
+
#
|
79
|
+
def content_length
|
80
|
+
(c = @headers["Content-Length"]) ? c.to_i : c
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns response <tt>Content-Type</tt> header.
|
84
|
+
#
|
85
|
+
# res.content_type = "text/html"
|
86
|
+
# res.content_type # => "text/html"
|
87
|
+
#
|
88
|
+
def content_type
|
89
|
+
@headers["Content-Type"]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Sets response <tt>Content-Type</tt> header to <tt>type</tt>.
|
93
|
+
#
|
94
|
+
# res.content_type = "text/html"
|
95
|
+
# res.content_type # => "text/html"
|
96
|
+
#
|
97
|
+
def content_type=(type)
|
98
|
+
@headers["Content-Type"] = type
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns response <tt>Location</tt> header.
|
102
|
+
#
|
103
|
+
# res.location = "/users"
|
104
|
+
# res.location # => "/users"
|
105
|
+
#
|
106
|
+
def location
|
107
|
+
@headers["Location"]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Sets response <tt>Location</tt> header to <tt>url</tt>.
|
111
|
+
#
|
112
|
+
# res.content_type = "text/html"
|
113
|
+
# res.content_type # => "text/html"
|
114
|
+
#
|
115
|
+
def location=(url)
|
116
|
+
@headers["Location"] = url
|
117
|
+
end
|
118
|
+
|
119
|
+
# Appends <tt>str</tt> to the response body and updates the
|
120
|
+
# <tt>Content-Length</tt> header.
|
121
|
+
#
|
122
|
+
# res.body # => []
|
123
|
+
#
|
124
|
+
# res.write("foo")
|
125
|
+
# res.write("bar")
|
126
|
+
#
|
127
|
+
# res.body
|
128
|
+
# # => ["foo", "bar"]
|
129
|
+
#
|
130
|
+
# res.headers["Content-Length"]
|
131
|
+
# # => 6
|
132
|
+
#
|
133
|
+
def write(str)
|
134
|
+
s = str.to_s
|
135
|
+
|
136
|
+
@length += s.bytesize
|
137
|
+
|
138
|
+
@headers["Content-Length"] = @length.to_s
|
139
|
+
|
140
|
+
@body << s
|
141
|
+
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns an Array with three elements: the status, headers and body.
|
146
|
+
# If the status is not set, the status is set to <tt>404</tt> if empty body,
|
147
|
+
# otherwise the status is set to <tt>200</tt>.
|
148
|
+
#
|
149
|
+
# res.status = 200
|
150
|
+
# res.finish
|
151
|
+
# # => [200, {}, []]
|
152
|
+
#
|
153
|
+
# res.status = nil
|
154
|
+
# res.finish
|
155
|
+
# # => [404, {}, []]
|
156
|
+
#
|
157
|
+
# res.status = nil
|
158
|
+
# res.content_type = "text/html"
|
159
|
+
# res.write("hei!")
|
160
|
+
# res.finish
|
161
|
+
# # => [200, { "Content-Type" => "text/html", "Content-Length" => "4" }, ["hei!"]]
|
162
|
+
#
|
163
|
+
def finish
|
164
|
+
@status ||= ((@body.empty?) ? 404 : 200)
|
165
|
+
|
166
|
+
[@status, @headers, @body]
|
167
|
+
end
|
168
|
+
|
169
|
+
# Sets the <tt>Location</tt> header to <tt>url</tt> and updates the status
|
170
|
+
# to <tt>status</tt>. By default, <tt>status</tt> is <tt>302</tt>.
|
171
|
+
#
|
172
|
+
# res.redirect("/path")
|
173
|
+
#
|
174
|
+
# res.status # => 302
|
175
|
+
# res.location # => "/path"
|
176
|
+
#
|
177
|
+
# res.redirect("https://google.com", 303)
|
178
|
+
#
|
179
|
+
# res.status # => 303
|
180
|
+
# res.location # => "https://google.com"
|
181
|
+
#
|
182
|
+
def redirect(path, status = 302)
|
183
|
+
self.status = status
|
184
|
+
self.location = path
|
185
|
+
end
|
186
|
+
|
187
|
+
# Sets a cookie into the response. The following options are supported:
|
188
|
+
#
|
189
|
+
# [domain and path]
|
190
|
+
# Define the scope of the cookie. They tell the browser what website
|
191
|
+
# the cookie belongs to. If a cookie's domain and path are not specified,
|
192
|
+
# they default to the domain and path of the resource that was requested.
|
193
|
+
#
|
194
|
+
# [expires]
|
195
|
+
# Defines a specific date and time for when the browser should delete
|
196
|
+
# the cookie.
|
197
|
+
#
|
198
|
+
# [max_age]
|
199
|
+
# Sets the cookie’s expiration as an interval of seconds in the future,
|
200
|
+
# relative to the time the browser received the cookie.
|
201
|
+
#
|
202
|
+
# [httponly]
|
203
|
+
# If <tt>true</tt>, sets the <tt>HttpOnly</tt> flag. This mitigates the
|
204
|
+
# risk of client side scripting accessing the cookie.
|
205
|
+
#
|
206
|
+
# [secure]
|
207
|
+
# If <tt>true</tt>, sets the <tt>Secure</tt> flag. This tells the browser
|
208
|
+
# to only transmit the cookie over HTTPS.
|
209
|
+
#
|
210
|
+
# [same_site]
|
211
|
+
# Disables third-party usage for cookies. There are two possible values
|
212
|
+
# <tt>:Lax</tt> and <tt>:Strict</tt>. In <tt>Strict</tt> mode, the cookie
|
213
|
+
# is restrain to any cross-site usage; in <tt>Lax</tt> mode, some cross-site
|
214
|
+
# usage is allowed.
|
215
|
+
#
|
216
|
+
# <tt></tt>
|
17
217
|
#
|
18
218
|
# res.set_cookie("foo", "bar")
|
19
|
-
# res["Set-Cookie"] # => "foo=bar"
|
219
|
+
# res.headers["Set-Cookie"] # => "foo=bar"
|
20
220
|
#
|
21
221
|
# res.set_cookie("foo2", "bar2")
|
22
|
-
# res["Set-Cookie"] # => "foo=bar\nfoo2=bar2"
|
222
|
+
# res.headers["Set-Cookie"] # => "foo=bar\nfoo2=bar2"
|
23
223
|
#
|
24
224
|
# res.set_cookie("bar", "bar", {
|
25
225
|
# domain: ".example.com",
|
@@ -31,8 +231,8 @@ class Tynn
|
|
31
231
|
# same_site: :Lax
|
32
232
|
# })
|
33
233
|
#
|
34
|
-
# res["Set-Cookie"].split("\n").last
|
35
|
-
# # => "bar=bar; domain=.example.com; path=/; secure; HttpOnly; SameSite=Lax
|
234
|
+
# res.headers["Set-Cookie"].split("\n").last
|
235
|
+
# # => "bar=bar; domain=.example.com; path=/; secure; HttpOnly; SameSite=Lax"
|
36
236
|
#
|
37
237
|
# *NOTE.* This method doesn't sign and/or encrypt the value of the cookie.
|
38
238
|
#
|
@@ -43,15 +243,17 @@ class Tynn
|
|
43
243
|
# Deletes cookie by <tt>key</tt>.
|
44
244
|
#
|
45
245
|
# res.set_cookie("foo", "bar")
|
46
|
-
# res["Set-Cookie"]
|
246
|
+
# res.headers["Set-Cookie"]
|
47
247
|
# # => "foo=bar"
|
48
248
|
#
|
49
249
|
# res.delete_cookie("foo")
|
50
|
-
# res["Set-Cookie"]
|
250
|
+
# res.headers["Set-Cookie"]
|
51
251
|
# # => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
|
52
252
|
#
|
253
|
+
# Check #set_cookie for supported options.
|
254
|
+
#
|
53
255
|
def delete_cookie(key, options = {})
|
54
|
-
Rack::Utils.delete_cookie_header!(headers, options)
|
256
|
+
Rack::Utils.delete_cookie_header!(headers, key, options)
|
55
257
|
end
|
56
258
|
end
|
57
259
|
end
|