tynn 2.0.0.alpha → 2.0.0.beta1
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 +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
|