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.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tynn
4
+ # Generic Tynn exception class.
5
+ class Error < StandardError
6
+ end
7
+ end
@@ -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 response body.
16
- # It automatically sets the <tt>Content-Type</tt> header to <tt>application/json</tt>.
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("hash") do
26
+ # on "hash" do
20
27
  # json(foo: "bar")
21
28
  # end
22
29
  #
23
- # on("array") do
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.headers["Content-Type"] = "application/json"
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
@@ -1,52 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tilt"
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
- layout: options.fetch(:layout, "layout"),
10
- views: options.fetch(:views, File.expand_path("views", Dir.pwd)),
11
- engine: options.fetch(:engine, "erb"),
12
- engine_opts: {
13
- escape_html: true
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
- def render(template, locals = {}, layout = self.class.settings.dig(:render, :layout))
20
- res.headers[Rack::CONTENT_TYPE] ||= Syro::Response::DEFAULT
17
+ include ::HMote::Helpers
21
18
 
22
- res.write(view(template, locals, layout))
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
- def view(template, locals = {}, layout = self.class.settings.dig(:render, :layout))
26
- partial(layout, locals.merge(content: partial(template, locals)))
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
- tilt(template_path(template), locals, self.class.settings.dig(:render, :engine_opts))
46
+ hmote(template_path(template), locals.merge(app: self), TOPLEVEL_BINDING)
31
47
  end
32
48
 
33
49
  private
34
50
 
35
- def tilt(file, locals = {}, opts = {})
36
- tilt_cache.fetch(file) {
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 tilt_cache
42
- Thread.current[:tilt_cache] ||= Tilt::Cache.new
55
+ def views_path
56
+ File.expand_path(render_opts[:views], render_opts[:root])
43
57
  end
44
58
 
45
- def template_path(template)
46
- dir = self.class.settings.dig(:render, :views)
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
@@ -5,18 +5,55 @@ class Tynn
5
5
  #
6
6
  # env = {
7
7
  # "REQUEST_METHOD" => "GET",
8
- # "QUERY_STRING" => "email=me@tynn.xyz"
8
+ # "QUERY_STRING" => "q=great",
9
+ # # ...
9
10
  # }
10
11
  #
11
12
  # req = Tynn::Request.new(env)
12
13
  #
13
- # req.get? # => true
14
- # req.post? # => false
15
- # req.params # => { "email" => "me@tynn.xyz" }
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 = Set.new(%w(
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
- )).freeze
75
+ ).freeze # :nodoc:
39
76
 
40
- def initialize(req)
41
- @req = req
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
- @req.env[transform_key(key)]
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
- @req.env.fetch(transform_key(key), *args, &block)
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
- @req.env.key?(transform_key(key))
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
@@ -6,20 +6,220 @@ class Tynn
6
6
  # res = Tynn::Response.new
7
7
  #
8
8
  # res.status = 200
9
- # res["Content-Type"] = "text/html"
10
- # res.write("foo")
9
+ # res.headers["Content-Type"] = "text/plain"
10
+ # res.write("hei!")
11
11
  #
12
12
  # res.finish
13
- # # => [200, { "Content-Type" => "text/html", "Content-Length" => 3 }, ["foo"]]
13
+ # # => [200, { "Content-Type" => "text/plain", "Content-Length" => 4 }, ["hei!"]]
14
14
  #
15
- class Response < Syro::Response
16
- # Sets a cookie into the response.
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