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.
@@ -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