syntropy 0.18.1 → 0.20
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/CHANGELOG.md +10 -0
- data/README.md +10 -0
- data/TODO.md +3 -0
- data/bin/syntropy +9 -1
- data/examples/templates.rb +16 -18
- data/lib/syntropy/app.rb +130 -10
- data/lib/syntropy/applets/builtin/debug/debug.js +1 -1
- data/lib/syntropy/request_extensions.rb +30 -0
- data/lib/syntropy/version.rb +1 -1
- data/syntropy.gemspec +2 -2
- data/test/bm_server.rb +1 -1
- data/test/test_app.rb +64 -1
- data/test/test_caching.rb +133 -0
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 652c2d240350e3a5d0ce18cdd73a76a2ea3920a58ca777f46c34aa8a6e567ecd
|
4
|
+
data.tar.gz: cd49374a2469e501ea9adec27db55f9b047a418e7f1291894a9dd68c7d2e367f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1861340956ffbf7db1b0163ec7a1fd84f1b2c422ae9d25ab7cfa531b70459e22e306876bba6e1865e1cc398475d536a5726dbe276744b127fe416851162fe62
|
7
|
+
data.tar.gz: 8ccf9cc252f7efa6e086ca830fbc9a6f836ecb43f91880d9c399aeb05fe8ca42677de6f1493785ef436c3b5a3f999ed50c7834c17b37ff1b779ed24539016ffd
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
# 0.20 2025-09-17
|
2
|
+
|
3
|
+
- Update Papercraft
|
4
|
+
|
5
|
+
# 0.19 2025-09-14
|
6
|
+
|
7
|
+
- Implement HTTP caching for static files
|
8
|
+
- Add `--no-server-headers` option
|
9
|
+
- Update TP2: server headers, injected response headers, cookies
|
10
|
+
|
1
11
|
# 0.18 2025-09-11
|
2
12
|
|
3
13
|
- Rename P2 back to Papercraft
|
data/README.md
CHANGED
@@ -17,6 +17,16 @@
|
|
17
17
|
</a>
|
18
18
|
</p>
|
19
19
|
|
20
|
+
```
|
21
|
+
ooo
|
22
|
+
ooooo
|
23
|
+
ooo vvv Syntropy - a web framework for Ruby
|
24
|
+
o vvvvv -------------------------------------
|
25
|
+
| vvv o
|
26
|
+
:|:::|::|:
|
27
|
+
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
28
|
+
```
|
29
|
+
|
20
30
|
## What is Syntropy?
|
21
31
|
|
22
32
|
| Syntropy: A tendency towards complexity, structure, order, organization of
|
data/TODO.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
## Immediate
|
2
2
|
|
3
|
+
- [ ] On internal error in module, default action should be to display page
|
4
|
+
showing error, with auto refresh on file change
|
5
|
+
- [ ] Do not log error backtrace for 404 errors (or other )
|
3
6
|
- [ ] Collection - treat directories and files as collections of data.
|
4
7
|
|
5
8
|
Kind of similar to the routing tree, but instead of routes it just takes a
|
data/bin/syntropy
CHANGED
@@ -8,7 +8,11 @@ env = {
|
|
8
8
|
mount_path: '/',
|
9
9
|
banner: Syntropy::BANNER,
|
10
10
|
logger: true,
|
11
|
-
builtin_applet_path: '/.syntropy'
|
11
|
+
builtin_applet_path: '/.syntropy',
|
12
|
+
server_extensions: {
|
13
|
+
date: true,
|
14
|
+
name: 'Syntropy'
|
15
|
+
}
|
12
16
|
}
|
13
17
|
|
14
18
|
parser = OptionParser.new do |o|
|
@@ -45,6 +49,10 @@ parser = OptionParser.new do |o|
|
|
45
49
|
env[:builtin_applet_path] = nil
|
46
50
|
end
|
47
51
|
|
52
|
+
o.on('--no-server-headers', 'Don\'t include Server and Date headers') do
|
53
|
+
env[:server_extensions] = nil
|
54
|
+
end
|
55
|
+
|
48
56
|
o.on('-v', '--version', 'Show version') do
|
49
57
|
require 'syntropy/version'
|
50
58
|
puts "Syntropy version #{Syntropy::VERSION}"
|
data/examples/templates.rb
CHANGED
@@ -3,24 +3,22 @@ Card = import 'card'
|
|
3
3
|
export template {
|
4
4
|
html5 {
|
5
5
|
head {
|
6
|
-
style
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
CSS
|
23
|
-
}
|
6
|
+
style <<~CSS
|
7
|
+
div {
|
8
|
+
display: grid;
|
9
|
+
grid-template-columns: 1fr 1fr;
|
10
|
+
}
|
11
|
+
span.foo {
|
12
|
+
color: white;
|
13
|
+
background-color: blue;
|
14
|
+
padding: 1em;
|
15
|
+
}
|
16
|
+
span.bar {
|
17
|
+
color: white;
|
18
|
+
background-color: green;
|
19
|
+
padding: 1em;
|
20
|
+
}
|
21
|
+
CSS
|
24
22
|
}
|
25
23
|
body {
|
26
24
|
p { a '< Home', href: '/' }
|
data/lib/syntropy/app.rb
CHANGED
@@ -81,8 +81,17 @@ module Syntropy
|
|
81
81
|
error_handler.(req, e)
|
82
82
|
end
|
83
83
|
|
84
|
+
# Returns the route entry for the given path. If compute_proc is true,
|
85
|
+
# computes the route proc if not yet computed.
|
86
|
+
#
|
87
|
+
# @param path [String] path
|
88
|
+
# @param params [Hash] hash receiving path parameters
|
89
|
+
# @param compute_proc [bool] whether to compute the route proc
|
90
|
+
# @return [Hash] route entry
|
84
91
|
def route(path, params = {}, compute_proc: false)
|
85
92
|
route = @router_proc.(path, params)
|
93
|
+
return if !route
|
94
|
+
|
86
95
|
route[:proc] ||= compute_route_proc(route) if compute_proc
|
87
96
|
route
|
88
97
|
end
|
@@ -101,6 +110,7 @@ module Syntropy
|
|
101
110
|
@router_proc = @routing_tree.router_proc
|
102
111
|
end
|
103
112
|
|
113
|
+
|
104
114
|
def mount_builtin_applet
|
105
115
|
path = @env[:builtin_applet_path]
|
106
116
|
@builtin_applet ||= Syntropy.builtin_applet(@env, mount_path: path)
|
@@ -117,6 +127,11 @@ module Syntropy
|
|
117
127
|
compose_up_tree_hooks(route, pure)
|
118
128
|
end
|
119
129
|
|
130
|
+
# Returns the pure route proc for the given route. A pure route proc is the
|
131
|
+
# computed proc for the route without any middleware hooks.
|
132
|
+
#
|
133
|
+
# @param route [Hash] route entry
|
134
|
+
# @return [Proc] route proc
|
120
135
|
def pure_route_proc(route)
|
121
136
|
case (kind = route[:target][:kind])
|
122
137
|
when :static
|
@@ -131,19 +146,90 @@ module Syntropy
|
|
131
146
|
end
|
132
147
|
|
133
148
|
# Returns a proc rendering the given static route
|
149
|
+
#
|
150
|
+
# @param [Hash] route entry
|
151
|
+
# @return [Proc] route handler proc
|
134
152
|
def static_route_proc(route)
|
135
153
|
fn = route[:target][:fn]
|
136
154
|
headers = { 'Content-Type' => Qeweney::MimeTypes[File.extname(fn)] }
|
137
155
|
|
138
156
|
->(req) {
|
139
|
-
req.
|
140
|
-
|
141
|
-
|
142
|
-
|
157
|
+
case req.method
|
158
|
+
when 'head'
|
159
|
+
req.respond(nil, headers)
|
160
|
+
when 'get'
|
161
|
+
serve_static_file(req, route[:target])
|
162
|
+
else
|
163
|
+
raise Syntropy::Error.method_not_allowed
|
164
|
+
end
|
143
165
|
}
|
144
166
|
end
|
145
167
|
|
146
|
-
#
|
168
|
+
# Serves a static file from the given target hash with cache validation.
|
169
|
+
#
|
170
|
+
# @param req [Qeweney::Request] request
|
171
|
+
# @param target [Hash] route target hash
|
172
|
+
# @return [void]
|
173
|
+
def serve_static_file(req, target)
|
174
|
+
validate_static_file_info(target)
|
175
|
+
cache_opts = {
|
176
|
+
cache_control: 'max-age=3600',
|
177
|
+
last_modified: target[:last_modified_date],
|
178
|
+
etag: target[:etag]
|
179
|
+
}
|
180
|
+
req.validate_cache(**cache_opts) {
|
181
|
+
req.respond(target[:content], 'Content-Type' => target[:mime_type])
|
182
|
+
}
|
183
|
+
rescue => e
|
184
|
+
p e
|
185
|
+
p e.backtrace
|
186
|
+
exit!
|
187
|
+
end
|
188
|
+
|
189
|
+
# Validates and conditionally updates the file information for the given
|
190
|
+
# target.
|
191
|
+
#
|
192
|
+
# @param target [Hash] route target hash
|
193
|
+
# @return [void]
|
194
|
+
def validate_static_file_info(target)
|
195
|
+
now = Time.now
|
196
|
+
return if target[:last_update] && ((Time.now - target[:last_update]) < 390)
|
197
|
+
|
198
|
+
update_static_file_info(target, now)
|
199
|
+
end
|
200
|
+
|
201
|
+
STATX_MASK = UM::STATX_MTIME | UM::STATX_SIZE
|
202
|
+
|
203
|
+
# Updates the static file information for the given target
|
204
|
+
#
|
205
|
+
# @param target [Hash] route target hash
|
206
|
+
# @param now [Time] current time
|
207
|
+
# @return [void]
|
208
|
+
def update_static_file_info(target, now)
|
209
|
+
target[:last_update] = now
|
210
|
+
fd = @machine.open(target[:fn], UM::O_RDONLY)
|
211
|
+
stat = @machine.statx(fd, nil, UM::AT_EMPTY_PATH, STATX_MASK)
|
212
|
+
target[:size] = size = stat[:size]
|
213
|
+
mtime = stat[:mtime].to_i
|
214
|
+
return if target[:last_modified] == mtime # file not modified
|
215
|
+
|
216
|
+
target[:last_modified] = mtime
|
217
|
+
target[:last_modified_date] = Time.at(mtime).httpdate
|
218
|
+
target[:content] = buffer = String.new(capacity: size)
|
219
|
+
target[:mime_type] = Qeweney::MimeTypes[File.extname(target[:fn])]
|
220
|
+
len = 0
|
221
|
+
while len < size
|
222
|
+
len += @machine.read(fd, buffer, size, len)
|
223
|
+
end
|
224
|
+
target[:etag] = Digest::SHA1.hexdigest(buffer)
|
225
|
+
ensure
|
226
|
+
@machine.close_async(fd) if fd
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns a proc rendering the given markdown route.
|
230
|
+
#
|
231
|
+
# @param route [Hash] route entry
|
232
|
+
# @return [Proc] route proc
|
147
233
|
def markdown_route_proc(route)
|
148
234
|
headers = { 'Content-Type' => 'text/html' }
|
149
235
|
|
@@ -155,27 +241,39 @@ module Syntropy
|
|
155
241
|
}
|
156
242
|
end
|
157
243
|
|
244
|
+
# Renders and returns the given markdown route as HTML.
|
245
|
+
#
|
246
|
+
# @param route [Hash] route entry
|
247
|
+
# @return [String] rendered HTML
|
158
248
|
def render_markdown(route)
|
159
249
|
atts, md = Syntropy.parse_markdown_file(route[:target][:fn], @env)
|
160
250
|
|
161
251
|
if (layout = atts[:layout])
|
162
252
|
route[:applied_layouts] ||= {}
|
163
|
-
proc = route[:applied_layouts][layout] ||=
|
253
|
+
proc = route[:applied_layouts][layout] ||= markdown_layout_template(layout)
|
164
254
|
html = proc.render(md:, **atts)
|
165
255
|
else
|
166
|
-
html =
|
256
|
+
html = default_markdown_layout_template.render(md:, **atts)
|
167
257
|
end
|
168
258
|
html
|
169
259
|
end
|
170
260
|
|
171
|
-
#
|
172
|
-
|
261
|
+
# Returns a markdown template based on the given layout.
|
262
|
+
#
|
263
|
+
# @param layout [String] layout name
|
264
|
+
# @return [Proc] layout template
|
265
|
+
def markdown_layout_template(layout)
|
173
266
|
@layouts ||= {}
|
174
267
|
template = @module_loader.load("_layout/#{layout}")
|
175
268
|
@layouts[layout] = template.apply { |md:, **| markdown(md) }
|
176
269
|
end
|
177
270
|
|
178
|
-
|
271
|
+
# Returns the default markdown layout, which renders to HTML and includes a
|
272
|
+
# title, the markdown content, and emits code for auto refreshing the page
|
273
|
+
# on file change.
|
274
|
+
#
|
275
|
+
# @return [Proc] default Markdown layout template
|
276
|
+
def default_markdown_layout_template
|
179
277
|
@default_markdown_layout ||= ->(md:, **atts) {
|
180
278
|
html5 {
|
181
279
|
head {
|
@@ -189,12 +287,22 @@ module Syntropy
|
|
189
287
|
}
|
190
288
|
end
|
191
289
|
|
290
|
+
# Returns the route proc for a module route.
|
291
|
+
#
|
292
|
+
# @param route [Hash] route entry
|
293
|
+
# @return [Proc] route proc
|
192
294
|
def module_route_proc(route)
|
193
295
|
ref = @routing_tree.fn_to_rel_path(route[:target][:fn])
|
194
296
|
mod = @module_loader.load(ref)
|
195
297
|
compute_module_proc(mod)
|
196
298
|
end
|
197
299
|
|
300
|
+
# Computes a route proc for the given module. If the module is a template,
|
301
|
+
# returns a route proc wrapping the template, otherwise the module itself is
|
302
|
+
# considered as the route proc.
|
303
|
+
#
|
304
|
+
# @param mod [any] module value
|
305
|
+
# @return [Proc] route proc
|
198
306
|
def compute_module_proc(mod)
|
199
307
|
case mod
|
200
308
|
when Papercraft::Template
|
@@ -204,6 +312,10 @@ module Syntropy
|
|
204
312
|
end
|
205
313
|
end
|
206
314
|
|
315
|
+
# Returns a route proc for the given template.
|
316
|
+
#
|
317
|
+
# @param template [Papercraft::Template] template
|
318
|
+
# @return [Proc] route proc
|
207
319
|
def papercraft_template_proc(template)
|
208
320
|
xml_mode = template.mode == :xml
|
209
321
|
template = template.proc
|
@@ -244,6 +356,11 @@ module Syntropy
|
|
244
356
|
(parent = route[:parent]) ? compose_up_tree_hooks(parent, proc) : proc
|
245
357
|
end
|
246
358
|
|
359
|
+
# Loads and returns an auxiliary module. This method is used for loading
|
360
|
+
# hook modules.
|
361
|
+
#
|
362
|
+
# @param hook_spec [Hash] hook spec
|
363
|
+
# @return [any] hook module
|
247
364
|
def load_aux_module(hook_spec)
|
248
365
|
ref = @routing_tree.fn_to_rel_path(hook_spec[:fn])
|
249
366
|
@module_loader.load(ref)
|
@@ -348,6 +465,9 @@ module Syntropy
|
|
348
465
|
end
|
349
466
|
end
|
350
467
|
|
468
|
+
# Signals a file change to any auto refresh watchers.
|
469
|
+
#
|
470
|
+
# @return [void]
|
351
471
|
def signal_auto_refresh_watchers!
|
352
472
|
return if !@builtin_applet
|
353
473
|
|
@@ -28,7 +28,7 @@
|
|
28
28
|
const attachment = document.createElement('debug-attachment');
|
29
29
|
const tag = ele.tagName;
|
30
30
|
if (tag == 'SCRIPT' || tag == 'HEAD') return;
|
31
|
-
|
31
|
+
|
32
32
|
const level = ele.dataset.syntropyLevel;
|
33
33
|
const href = ele.dataset.syntropyLoc;
|
34
34
|
const fn = ele.dataset.syntropyFn;
|
@@ -140,6 +140,36 @@ module Syntropy
|
|
140
140
|
value
|
141
141
|
end
|
142
142
|
|
143
|
+
# Validates request cache information. If the request cache information
|
144
|
+
# matches the given etag or last_modified values, responds with a 304 Not
|
145
|
+
# Modified status. Otherwise, yields to the given block for a normal
|
146
|
+
# response, and sets cache control headers according to the given arguments.
|
147
|
+
#
|
148
|
+
# @param cache_control [String] value for Cache-Control header
|
149
|
+
# @param etag [String, nil] Etag header value
|
150
|
+
# @param last_modified [String, nil] Last-Modified header value
|
151
|
+
# @return [void]
|
152
|
+
def validate_cache(cache_control: 'public', etag: nil, last_modified: nil)
|
153
|
+
validated = false
|
154
|
+
if (client_etag = headers['if-none-match'])
|
155
|
+
validated = true if client_etag == etag
|
156
|
+
end
|
157
|
+
if (client_mtime = headers['if-modified-since'])
|
158
|
+
validated = true if client_mtime == last_modified
|
159
|
+
end
|
160
|
+
if validated
|
161
|
+
respond(nil, ':status' => Qeweney::Status::NOT_MODIFIED)
|
162
|
+
else
|
163
|
+
cache_headers = {
|
164
|
+
'Cache-Control' => cache_control
|
165
|
+
}
|
166
|
+
cache_headers['Etag'] = etag if etag
|
167
|
+
cache_headers['Last-Modified'] = last_modified if last_modified
|
168
|
+
set_response_headers(cache_headers)
|
169
|
+
yield
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
143
173
|
# Reads the request body and returns form data.
|
144
174
|
#
|
145
175
|
# @return [Hash] form data
|
data/lib/syntropy/version.rb
CHANGED
data/syntropy.gemspec
CHANGED
@@ -23,9 +23,9 @@ Gem::Specification.new do |s|
|
|
23
23
|
|
24
24
|
s.add_dependency 'extralite', '2.13'
|
25
25
|
s.add_dependency 'json', '2.13.2'
|
26
|
-
s.add_dependency 'papercraft', '2.
|
26
|
+
s.add_dependency 'papercraft', '2.14'
|
27
27
|
s.add_dependency 'qeweney', '0.22'
|
28
|
-
s.add_dependency 'tp2', '0.
|
28
|
+
s.add_dependency 'tp2', '0.18'
|
29
29
|
s.add_dependency 'uringmachine', '0.18'
|
30
30
|
|
31
31
|
s.add_dependency 'listen', '3.9.0'
|
data/test/bm_server.rb
CHANGED
data/test/test_app.rb
CHANGED
@@ -117,7 +117,6 @@ class AppTest < Minitest::Test
|
|
117
117
|
|
118
118
|
req = make_request(':method' => 'GET', ':path' => '/test/rss')
|
119
119
|
assert_equal '<link>foo</link>', req.response_body
|
120
|
-
|
121
120
|
end
|
122
121
|
|
123
122
|
def test_app_file_watching
|
@@ -208,3 +207,67 @@ class MultiSiteAppTest < Minitest::Test
|
|
208
207
|
assert_equal '<h1>bar.baz</h1>', req.response_body.chomp
|
209
208
|
end
|
210
209
|
end
|
210
|
+
|
211
|
+
class AppAPITest < Minitest::Test
|
212
|
+
Status = Qeweney::Status
|
213
|
+
|
214
|
+
APP_ROOT = File.join(__dir__, 'app')
|
215
|
+
|
216
|
+
def setup
|
217
|
+
@machine = UM.new
|
218
|
+
|
219
|
+
@tmp_path = '/test/tmp'
|
220
|
+
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
221
|
+
|
222
|
+
@app = Syntropy::App.new(
|
223
|
+
root_dir: APP_ROOT,
|
224
|
+
mount_path: '/test',
|
225
|
+
watch_files: 0.05,
|
226
|
+
machine: @machine
|
227
|
+
)
|
228
|
+
end
|
229
|
+
|
230
|
+
def test_route_method
|
231
|
+
route = @app.route('/')
|
232
|
+
assert_nil route
|
233
|
+
|
234
|
+
route = @app.route('/', compute_proc: true)
|
235
|
+
assert_nil route
|
236
|
+
|
237
|
+
route = @app.route('/test')
|
238
|
+
assert_nil route[:parent]
|
239
|
+
assert_equal '/test', route[:path]
|
240
|
+
assert_equal :static, route[:target][:kind]
|
241
|
+
assert_equal File.join(APP_ROOT, 'index.html'), route[:target][:fn]
|
242
|
+
|
243
|
+
route = @app.route('/test/assets/style.css')
|
244
|
+
assert_equal '/test/assets', route[:parent][:path]
|
245
|
+
assert_equal :static, route[:target][:kind]
|
246
|
+
assert_equal File.join(APP_ROOT, 'assets/style.css'), route[:target][:fn]
|
247
|
+
|
248
|
+
route = @app.route('/test/api')
|
249
|
+
assert_equal '/test', route[:parent][:path]
|
250
|
+
assert_equal :module, route[:target][:kind]
|
251
|
+
assert_equal File.join(APP_ROOT, 'api+.rb'), route[:target][:fn]
|
252
|
+
|
253
|
+
route = @app.route('/test/api/foo')
|
254
|
+
assert_equal '/test', route[:parent][:path]
|
255
|
+
assert_equal :module, route[:target][:kind]
|
256
|
+
assert_equal File.join(APP_ROOT, 'api+.rb'), route[:target][:fn]
|
257
|
+
|
258
|
+
route = @app.route('/test/bar')
|
259
|
+
assert_equal '/test', route[:parent][:path]
|
260
|
+
assert_equal :module, route[:target][:kind]
|
261
|
+
assert_equal File.join(APP_ROOT, 'bar.rb'), route[:target][:fn]
|
262
|
+
|
263
|
+
route = @app.route('/test/about/raise')
|
264
|
+
assert_equal '/test/about', route[:parent][:path]
|
265
|
+
assert_equal :module, route[:target][:kind]
|
266
|
+
assert_equal File.join(APP_ROOT, 'about/raise.rb'), route[:target][:fn]
|
267
|
+
|
268
|
+
route = @app.route('/test/about/foo')
|
269
|
+
assert_equal '/test/about', route[:parent][:path]
|
270
|
+
assert_equal :markdown, route[:target][:kind]
|
271
|
+
assert_equal File.join(APP_ROOT, 'about/foo.md'), route[:target][:fn]
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
require 'digest/sha1'
|
5
|
+
|
6
|
+
class CachingTest < Minitest::Test
|
7
|
+
Status = Qeweney::Status
|
8
|
+
|
9
|
+
APP_ROOT = File.join(__dir__, 'app')
|
10
|
+
|
11
|
+
def make_socket_pair
|
12
|
+
port = SecureRandom.random_number(10000..40000)
|
13
|
+
server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
14
|
+
@machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
|
15
|
+
@machine.bind(server_fd, '127.0.0.1', port)
|
16
|
+
@machine.listen(server_fd, UM::SOMAXCONN)
|
17
|
+
|
18
|
+
client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
|
19
|
+
@machine.connect(client_conn_fd, '127.0.0.1', port)
|
20
|
+
|
21
|
+
server_conn_fd = @machine.accept(server_fd)
|
22
|
+
|
23
|
+
@machine.close(server_fd)
|
24
|
+
[client_conn_fd, server_conn_fd]
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup
|
28
|
+
@machine = UM.new
|
29
|
+
|
30
|
+
@tmp_path = '/test/tmp'
|
31
|
+
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
32
|
+
|
33
|
+
@env = {
|
34
|
+
machine: @machine,
|
35
|
+
root_dir: APP_ROOT,
|
36
|
+
mount_path: '/test',
|
37
|
+
watch_files: 0.05
|
38
|
+
}
|
39
|
+
|
40
|
+
@app = Syntropy::App.new(**@env)
|
41
|
+
|
42
|
+
@c_fd, @s_fd = make_socket_pair
|
43
|
+
@adapter = TP2::Connection.new(nil, @machine, @s_fd, @env) { @app.(it) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def teardown
|
47
|
+
@machine.close(@c_fd) rescue nil
|
48
|
+
@machine.close(@s_fd) rescue nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def write_http_request(msg, shutdown_wr = false)
|
52
|
+
@machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
53
|
+
@machine.shutdown(@c_fd, UM::SHUT_WR) if shutdown_wr
|
54
|
+
end
|
55
|
+
|
56
|
+
def write_client_side(msg)
|
57
|
+
@machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_client_side(len = 65536)
|
61
|
+
buf = +''
|
62
|
+
res = @machine.recv(@c_fd, buf, len, 0)
|
63
|
+
res == 0 ? nil : buf
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_static_file_caching
|
67
|
+
fn = File.join(APP_ROOT, 'assets/style.css')
|
68
|
+
stat = @machine.statx(UM::AT_FDCWD, fn, 0, UM::STATX_ALL)
|
69
|
+
content = IO.read(fn)
|
70
|
+
etag = Digest::SHA1.hexdigest(content)
|
71
|
+
last_modified = Time.at(stat[:mtime]).httpdate
|
72
|
+
size = stat[:size]
|
73
|
+
|
74
|
+
write_http_request "GET /test/assets/style.css HTTP/1.1\r\n\r\n"
|
75
|
+
@adapter.serve_request
|
76
|
+
response = read_client_side
|
77
|
+
|
78
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nCache-Control: max-age=3600\r\nEtag: #{etag}\r\nLast-Modified: #{last_modified}\r\nContent-Type: text/css\r\n\r\n#{size.to_s(16)}\r\n#{content}\r\n0\r\n\r\n"
|
79
|
+
assert_equal expected, response
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_static_file_caching_validate_etag
|
83
|
+
fn = File.join(APP_ROOT, 'assets/style.css')
|
84
|
+
stat = @machine.statx(UM::AT_FDCWD, fn, 0, UM::STATX_ALL)
|
85
|
+
content = IO.read(fn)
|
86
|
+
etag = Digest::SHA1.hexdigest(content)
|
87
|
+
last_modified = Time.at(stat[:mtime]).httpdate
|
88
|
+
size = stat[:size]
|
89
|
+
|
90
|
+
# bad etag
|
91
|
+
write_http_request "GET /test/assets/style.css HTTP/1.1\r\nIf-None-Match: foo\r\n\r\n"
|
92
|
+
@adapter.serve_request
|
93
|
+
response = read_client_side
|
94
|
+
|
95
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nCache-Control: max-age=3600\r\nEtag: #{etag}\r\nLast-Modified: #{last_modified}\r\nContent-Type: text/css\r\n\r\n#{size.to_s(16)}\r\n#{content}\r\n0\r\n\r\n"
|
96
|
+
assert_equal expected, response
|
97
|
+
|
98
|
+
# good etag
|
99
|
+
@adapter.response_headers.clear
|
100
|
+
write_http_request "GET /test/assets/style.css HTTP/1.1\r\nIf-None-Match: #{etag}\r\n\r\n"
|
101
|
+
@adapter.serve_request
|
102
|
+
response = read_client_side
|
103
|
+
|
104
|
+
expected = "HTTP/1.1 304\r\nContent-Length: 0\r\n\r\n"
|
105
|
+
assert_equal expected, response
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_static_file_caching_validate_last_modified
|
109
|
+
fn = File.join(APP_ROOT, 'assets/style.css')
|
110
|
+
stat = @machine.statx(UM::AT_FDCWD, fn, 0, UM::STATX_ALL)
|
111
|
+
content = IO.read(fn)
|
112
|
+
etag = Digest::SHA1.hexdigest(content)
|
113
|
+
last_modified = Time.at(stat[:mtime]).httpdate
|
114
|
+
size = stat[:size]
|
115
|
+
|
116
|
+
# bad stamp
|
117
|
+
write_http_request "GET /test/assets/style.css HTTP/1.1\r\nIf-Modified-Since: foo\r\n\r\n"
|
118
|
+
@adapter.serve_request
|
119
|
+
response = read_client_side
|
120
|
+
|
121
|
+
expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\nCache-Control: max-age=3600\r\nEtag: #{etag}\r\nLast-Modified: #{last_modified}\r\nContent-Type: text/css\r\n\r\n#{size.to_s(16)}\r\n#{content}\r\n0\r\n\r\n"
|
122
|
+
assert_equal expected, response
|
123
|
+
|
124
|
+
# good etag
|
125
|
+
@adapter.response_headers.clear
|
126
|
+
write_http_request "GET /test/assets/style.css HTTP/1.1\r\nIf-Modified-Since: #{last_modified}\r\n\r\n"
|
127
|
+
@adapter.serve_request
|
128
|
+
response = read_client_side
|
129
|
+
|
130
|
+
expected = "HTTP/1.1 304\r\nContent-Length: 0\r\n\r\n"
|
131
|
+
assert_equal expected, response
|
132
|
+
end
|
133
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syntropy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.20'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -43,14 +43,14 @@ dependencies:
|
|
43
43
|
requirements:
|
44
44
|
- - '='
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '2.
|
46
|
+
version: '2.14'
|
47
47
|
type: :runtime
|
48
48
|
prerelease: false
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - '='
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '2.
|
53
|
+
version: '2.14'
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
55
|
name: qeweney
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -71,14 +71,14 @@ dependencies:
|
|
71
71
|
requirements:
|
72
72
|
- - '='
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: '0.
|
74
|
+
version: '0.18'
|
75
75
|
type: :runtime
|
76
76
|
prerelease: false
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
79
|
- - '='
|
80
80
|
- !ruby/object:Gem::Version
|
81
|
-
version: '0.
|
81
|
+
version: '0.18'
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
83
|
name: uringmachine
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
@@ -248,6 +248,7 @@ files:
|
|
248
248
|
- test/helper.rb
|
249
249
|
- test/run.rb
|
250
250
|
- test/test_app.rb
|
251
|
+
- test/test_caching.rb
|
251
252
|
- test/test_connection_pool.rb
|
252
253
|
- test/test_errors.rb
|
253
254
|
- test/test_file_watch.rb
|