syntropy 0.33.0 → 0.34.0
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 +7 -0
- data/cmd/console.rb +18 -7
- data/cmd/serve.rb +26 -18
- data/cmd/test.rb +37 -24
- data/examples/blog/.gitignore +1 -0
- data/examples/blog/app/_lib/database.rb +13 -0
- data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
- data/examples/blog/app/posts/[id]/edit.rb +2 -2
- data/examples/blog/app/posts/[id]/index.rb +4 -4
- data/examples/blog/app/posts/index.rb +4 -4
- data/examples/blog/app/posts/new.rb +1 -1
- data/examples/blog/config/development.rb +5 -0
- data/examples/blog/config/production.rb +4 -0
- data/examples/blog/config/test.rb +5 -0
- data/examples/blog/test/test_posts.rb +65 -0
- data/examples/mcp-oauth/app/oauth/token.rb +1 -1
- data/lib/syntropy/app.rb +48 -40
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
- data/lib/syntropy/db/schema.rb +1 -1
- data/lib/syntropy/db/store.rb +2 -0
- data/lib/syntropy/errors.rb +6 -2
- data/lib/syntropy/http/client.rb +1 -0
- data/lib/syntropy/http/server_connection.rb +0 -4
- data/lib/syntropy/json_api.rb +27 -1
- data/lib/syntropy/logger.rb +81 -27
- data/lib/syntropy/markdown.rb +61 -32
- data/lib/syntropy/mime_types.rb +9 -5
- data/lib/syntropy/module_loader.rb +20 -9
- data/lib/syntropy/papercraft_extensions.rb +2 -2
- data/lib/syntropy/request/mock_adapter.rb +10 -8
- data/lib/syntropy/request/request_info.rb +91 -0
- data/lib/syntropy/request/response.rb +1 -12
- data/lib/syntropy/request/validation.rb +1 -0
- data/lib/syntropy/request.rb +51 -19
- data/lib/syntropy/routing_tree.rb +27 -28
- data/lib/syntropy/session.rb +198 -0
- data/lib/syntropy/side_run.rb +25 -2
- data/lib/syntropy/test.rb +105 -10
- data/lib/syntropy/utils.rb +53 -18
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +44 -10
- data/test/bm_router_proc.rb +4 -4
- data/test/fixtures/app/class_instance.rb +5 -0
- data/test/fixtures/app/http.rb +5 -0
- data/test/fixtures/app/post_ct.rb +5 -0
- data/test/fixtures/app/singleton.rb +3 -0
- data/test/test_app.rb +13 -52
- data/test/test_caching.rb +2 -2
- data/test/test_db_schema.rb +1 -1
- data/test/test_http_server_connection.rb +3 -3
- data/test/test_module_loader.rb +5 -2
- data/test/test_response.rb +0 -19
- data/test/test_routing_tree.rb +69 -69
- data/test/test_server.rb +5 -9
- data/test/test_test.rb +70 -0
- metadata +52 -42
- data/examples/blog/app/_setup.rb +0 -4
- data/lib/syntropy/request/session.rb +0 -113
- /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
- /data/test/{app → fixtures/app}/_hook.rb +0 -0
- /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
- /data/test/{app → fixtures/app}/about/_error.rb +0 -0
- /data/test/{app → fixtures/app}/about/foo.md +0 -0
- /data/test/{app → fixtures/app}/about/index.rb +0 -0
- /data/test/{app → fixtures/app}/about/raise.rb +0 -0
- /data/test/{app → fixtures/app}/api+.rb +0 -0
- /data/test/{app → fixtures/app}/assets/style.css +0 -0
- /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
- /data/test/{app → fixtures/app}/bar.rb +0 -0
- /data/test/{app → fixtures/app}/baz.rb +0 -0
- /data/test/{app → fixtures/app}/by_method.rb +0 -0
- /data/test/{app → fixtures/app}/deps.rb +0 -0
- /data/test/{app → fixtures/app}/index.html +0 -0
- /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
- /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
- /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
- /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
- /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
- /data/test/{app → fixtures/app}/rss.rb +0 -0
- /data/test/{app → fixtures/app}/tmp.rb +0 -0
- /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
- /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
- /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
- /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
- /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
- /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
- /data/test/{schema → fixtures/schema}/2026-05-30-bar.rb +0 -0
|
@@ -28,7 +28,7 @@ module Syntropy
|
|
|
28
28
|
# @return [void]
|
|
29
29
|
def initialize(env)
|
|
30
30
|
@env = env
|
|
31
|
-
@
|
|
31
|
+
@app_root = env[:app_root]
|
|
32
32
|
@modules = {} # maps ref to module entry
|
|
33
33
|
@fn_map = {} # maps filename to ref
|
|
34
34
|
end
|
|
@@ -55,8 +55,8 @@ module Syntropy
|
|
|
55
55
|
# @param dir [String] relative module directory
|
|
56
56
|
# @return [Array<String>] list of modules
|
|
57
57
|
def list(dir)
|
|
58
|
-
fns = Dir[File.join(@
|
|
59
|
-
fns.map { it.match(/^#{@
|
|
58
|
+
fns = Dir[File.join(@app_root, dir, '*.rb')]
|
|
59
|
+
fns.map { it.match(/^#{@app_root}\/(.+)\.rb$/)[1] }.sort
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Invalidates a module by its filename, normally following a change to the
|
|
@@ -89,10 +89,13 @@ module Syntropy
|
|
|
89
89
|
entry[:reverse_deps].each { invalidate_ref(it) }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# Invalidates a collection module.
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
92
95
|
def invalidate_collection_modules
|
|
93
96
|
refs = []
|
|
94
97
|
@modules.each do |ref, entry|
|
|
95
|
-
refs << ref if entry[:module].
|
|
98
|
+
refs << ref if entry[:module].collection_module?
|
|
96
99
|
end
|
|
97
100
|
refs.each { invalidate_ref(it) }
|
|
98
101
|
end
|
|
@@ -118,7 +121,7 @@ module Syntropy
|
|
|
118
121
|
# @return [Hash] module entry
|
|
119
122
|
def load_module(ref, raise_on_missing: true)
|
|
120
123
|
ref = "/#{ref}" if ref !~ /^\//
|
|
121
|
-
fn = File.expand_path(File.join(@
|
|
124
|
+
fn = File.expand_path(File.join(@app_root, "#{ref}.rb"))
|
|
122
125
|
if !File.file?(fn)
|
|
123
126
|
raise Syntropy::Error, "File not found #{fn}" if raise_on_missing
|
|
124
127
|
|
|
@@ -142,6 +145,10 @@ module Syntropy
|
|
|
142
145
|
}
|
|
143
146
|
end
|
|
144
147
|
|
|
148
|
+
# Cleans up a module reference specifier, turning /index into /
|
|
149
|
+
#
|
|
150
|
+
# @param ref [String] input ref
|
|
151
|
+
# @return [String] clean ref
|
|
145
152
|
def clean_ref(ref)
|
|
146
153
|
return '/' if ref =~ /^index(\+)?$/
|
|
147
154
|
|
|
@@ -222,7 +229,7 @@ module Syntropy
|
|
|
222
229
|
# #collection_module!
|
|
223
230
|
#
|
|
224
231
|
# @return [bool]
|
|
225
|
-
def
|
|
232
|
+
def collection_module?
|
|
226
233
|
@collection_module_p
|
|
227
234
|
end
|
|
228
235
|
|
|
@@ -258,6 +265,10 @@ module Syntropy
|
|
|
258
265
|
self
|
|
259
266
|
end
|
|
260
267
|
|
|
268
|
+
# Normalize an import reference, turning a relative path into an absolute one.
|
|
269
|
+
#
|
|
270
|
+
# @param ref [String] input ref
|
|
271
|
+
# @return [String] normalized ref
|
|
261
272
|
def normalize_import_ref(ref)
|
|
262
273
|
base = @ref == '' ? '/' : @ref
|
|
263
274
|
if ref =~ /^\//
|
|
@@ -274,7 +285,7 @@ module Syntropy
|
|
|
274
285
|
# @return [Papercraft::Template] template
|
|
275
286
|
def template(proc = nil, &block)
|
|
276
287
|
proc ||= block
|
|
277
|
-
raise
|
|
288
|
+
raise 'No template block/proc given' if !proc
|
|
278
289
|
|
|
279
290
|
Papercraft::Template.new(proc)
|
|
280
291
|
end
|
|
@@ -286,10 +297,10 @@ module Syntropy
|
|
|
286
297
|
# @return [Papercraft::Template] template
|
|
287
298
|
def template_xml(proc = nil, &block)
|
|
288
299
|
proc ||= block
|
|
289
|
-
raise
|
|
300
|
+
raise 'No template block/proc given' if !proc
|
|
290
301
|
|
|
291
302
|
Papercraft::Template.new(proc, mode: :xml)
|
|
292
|
-
rescue => e
|
|
303
|
+
rescue StandardError => e
|
|
293
304
|
p e
|
|
294
305
|
p e.backtrace
|
|
295
306
|
raise
|
|
@@ -4,12 +4,12 @@ require 'papercraft'
|
|
|
4
4
|
|
|
5
5
|
Papercraft.extension(
|
|
6
6
|
'auto_refresh!': ->(loc = '/.syntropy') {
|
|
7
|
-
if
|
|
7
|
+
if Syntropy.dev_mode
|
|
8
8
|
script(src: File.join(loc, 'auto_refresh/watch.js'), type: 'module')
|
|
9
9
|
end
|
|
10
10
|
},
|
|
11
11
|
'debug_template!': ->(loc = '/.syntropy') {
|
|
12
|
-
if
|
|
12
|
+
if Syntropy.dev_mode
|
|
13
13
|
script(src: File.join(loc, 'debug/debug.js'), type: 'module')
|
|
14
14
|
end
|
|
15
15
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Syntropy
|
|
4
|
+
# Implements a mock adapter for testing
|
|
4
5
|
class MockAdapter
|
|
5
6
|
attr_reader :response_body, :response_headers, :calls
|
|
6
7
|
|
|
@@ -19,14 +20,13 @@ module Syntropy
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def initialize(request_body)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
23
|
+
@request_body_chunks =
|
|
24
|
+
case request_body
|
|
25
|
+
when Array then request_body
|
|
26
|
+
when nil then []
|
|
27
|
+
else [request_body]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
30
|
@calls = []
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -47,6 +47,8 @@ module Syntropy
|
|
|
47
47
|
response_headers[':status'] || HTTP::OK
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def respond_to_missing?(sym) = true
|
|
51
|
+
|
|
50
52
|
def method_missing(sym, *args)
|
|
51
53
|
calls << [sym, *args]
|
|
52
54
|
end
|
|
@@ -3,36 +3,61 @@
|
|
|
3
3
|
require 'uri'
|
|
4
4
|
|
|
5
5
|
module Syntropy
|
|
6
|
+
# Request information extension methods.
|
|
6
7
|
module RequestInfoMethods
|
|
8
|
+
# Returns the request host.
|
|
9
|
+
#
|
|
10
|
+
# @return [String, nil]
|
|
7
11
|
def host
|
|
8
12
|
@headers['host'] || @headers[':authority']
|
|
9
13
|
end
|
|
10
14
|
alias_method :authority, :host
|
|
11
15
|
|
|
16
|
+
# Returns the connection header value.
|
|
17
|
+
#
|
|
18
|
+
# @return [String, nil]
|
|
12
19
|
def connection
|
|
13
20
|
@headers['connection']
|
|
14
21
|
end
|
|
15
22
|
|
|
23
|
+
# Returns the upgrade protocol.
|
|
24
|
+
#
|
|
25
|
+
# @return [String, nil]
|
|
16
26
|
def upgrade_protocol
|
|
17
27
|
connection == 'upgrade' && @headers['upgrade']&.downcase
|
|
18
28
|
end
|
|
19
29
|
|
|
30
|
+
# Returns the websocket version.
|
|
31
|
+
#
|
|
32
|
+
# @return [String, nil]
|
|
20
33
|
def websocket_version
|
|
21
34
|
headers['sec-websocket-version'].to_i
|
|
22
35
|
end
|
|
23
36
|
|
|
37
|
+
# Returns the protocol.
|
|
38
|
+
#
|
|
39
|
+
# @return [String, nil]
|
|
24
40
|
def protocol
|
|
25
41
|
@protocol ||= @adapter.protocol
|
|
26
42
|
end
|
|
27
43
|
|
|
44
|
+
# Returns the HTTP method in lower case.
|
|
45
|
+
#
|
|
46
|
+
# @return [String]
|
|
28
47
|
def method
|
|
29
48
|
@method ||= @headers[':method'].downcase
|
|
30
49
|
end
|
|
31
50
|
|
|
51
|
+
# Returns the request scheme.
|
|
52
|
+
#
|
|
53
|
+
# @return [String, nil]
|
|
32
54
|
def scheme
|
|
33
55
|
@scheme ||= @headers[':scheme']
|
|
34
56
|
end
|
|
35
57
|
|
|
58
|
+
# Returns the request content type.
|
|
59
|
+
#
|
|
60
|
+
# @return [String, nil]
|
|
36
61
|
def content_type
|
|
37
62
|
ct = @headers['content-type']
|
|
38
63
|
return nil if !ct
|
|
@@ -59,22 +84,37 @@ module Syntropy
|
|
|
59
84
|
self
|
|
60
85
|
end
|
|
61
86
|
|
|
87
|
+
# Returns the parsed request URI.
|
|
88
|
+
#
|
|
89
|
+
# @return [URI::Generic]
|
|
62
90
|
def uri
|
|
63
91
|
@uri ||= URI.parse(@headers[':path'] || '')
|
|
64
92
|
end
|
|
65
93
|
|
|
94
|
+
# Returns the parsed full request URI.
|
|
95
|
+
#
|
|
96
|
+
# @return [URI::HTTP]
|
|
66
97
|
def full_uri
|
|
67
98
|
@full_uri = "#{scheme}://#{host}#{uri}"
|
|
68
99
|
end
|
|
69
100
|
|
|
101
|
+
# Returns the request path.
|
|
102
|
+
#
|
|
103
|
+
# @return [String]
|
|
70
104
|
def path
|
|
71
105
|
@path ||= uri.path
|
|
72
106
|
end
|
|
73
107
|
|
|
108
|
+
# Returns the request (unparsed) query string.
|
|
109
|
+
#
|
|
110
|
+
# @return [String, nil]
|
|
74
111
|
def query_string
|
|
75
112
|
@query_string ||= uri.query
|
|
76
113
|
end
|
|
77
114
|
|
|
115
|
+
# Returns the parsed query hash.
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash]
|
|
78
118
|
def query
|
|
79
119
|
return @query if @query
|
|
80
120
|
|
|
@@ -83,6 +123,10 @@ module Syntropy
|
|
|
83
123
|
|
|
84
124
|
QUERY_KV_REGEXP = /([^=]+)(?:=(.*))?/
|
|
85
125
|
|
|
126
|
+
# Converts a query string into a query hash
|
|
127
|
+
#
|
|
128
|
+
# @param query [String]
|
|
129
|
+
# @return [Hash]
|
|
86
130
|
def parse_query(query)
|
|
87
131
|
query.split('&').each_with_object({}) do |kv, h|
|
|
88
132
|
k, v = kv.match(QUERY_KV_REGEXP)[1..2]
|
|
@@ -90,10 +134,16 @@ module Syntropy
|
|
|
90
134
|
end
|
|
91
135
|
end
|
|
92
136
|
|
|
137
|
+
# Returns the request ID.
|
|
138
|
+
#
|
|
139
|
+
# @return [String, nil]
|
|
93
140
|
def request_id
|
|
94
141
|
@headers['x-request-id']
|
|
95
142
|
end
|
|
96
143
|
|
|
144
|
+
# Returns the forwarded for value.
|
|
145
|
+
#
|
|
146
|
+
# @return [String, nil]
|
|
97
147
|
def forwarded_for
|
|
98
148
|
@headers['x-forwarded-for']
|
|
99
149
|
end
|
|
@@ -107,6 +157,9 @@ module Syntropy
|
|
|
107
157
|
encoding.split(',').map { |i| i.strip }
|
|
108
158
|
end
|
|
109
159
|
|
|
160
|
+
# Returns the parsed cookie values.
|
|
161
|
+
#
|
|
162
|
+
# @return [String, nil]
|
|
110
163
|
def cookies
|
|
111
164
|
@cookies ||= parse_cookies(headers['cookie'])
|
|
112
165
|
end
|
|
@@ -114,6 +167,10 @@ module Syntropy
|
|
|
114
167
|
COOKIE_RE = /^([^=]+)=(.*)$/.freeze
|
|
115
168
|
SEMICOLON = ';'
|
|
116
169
|
|
|
170
|
+
# Parses the cookie string.
|
|
171
|
+
#
|
|
172
|
+
# @param cookies [String]
|
|
173
|
+
# @return [Hash]
|
|
117
174
|
def parse_cookies(cookies)
|
|
118
175
|
return {} unless cookies
|
|
119
176
|
|
|
@@ -139,6 +196,9 @@ module Syntropy
|
|
|
139
196
|
raise Syntropy::Error.new('Invalid form data', HTTP::BAD_REQUEST)
|
|
140
197
|
end
|
|
141
198
|
|
|
199
|
+
# Returns true if the user-agent is a browser.
|
|
200
|
+
#
|
|
201
|
+
# @return [bool]
|
|
142
202
|
def browser?
|
|
143
203
|
user_agent = headers['user-agent']
|
|
144
204
|
user_agent && user_agent =~ /^Mozilla\//
|
|
@@ -156,6 +216,9 @@ module Syntropy
|
|
|
156
216
|
@accept_parts.include?(mime_type)
|
|
157
217
|
end
|
|
158
218
|
|
|
219
|
+
# Returns the bearer token.
|
|
220
|
+
#
|
|
221
|
+
# @return [String, nil]
|
|
159
222
|
def auth_bearer_token
|
|
160
223
|
auth = headers['authorization']
|
|
161
224
|
if auth && (m = auth.match(/Bearer\s+([^\w]+)/))
|
|
@@ -167,12 +230,22 @@ module Syntropy
|
|
|
167
230
|
|
|
168
231
|
private
|
|
169
232
|
|
|
233
|
+
# Parses an accept string into an array of accepted MIME types.
|
|
234
|
+
#
|
|
235
|
+
# @param accept [string]
|
|
236
|
+
# @return [Array<String>]
|
|
170
237
|
def parse_accept_parts(accept)
|
|
171
238
|
accept.split(',').map { it.match(/^\s*([^\s;]+)/)[1] }
|
|
172
239
|
end
|
|
173
240
|
end
|
|
174
241
|
|
|
242
|
+
# Request info class methods
|
|
175
243
|
module RequestInfoClassMethods
|
|
244
|
+
# Parses form data into a hash
|
|
245
|
+
#
|
|
246
|
+
# @param body [String]
|
|
247
|
+
# @param headers [Hash]
|
|
248
|
+
# @return [Hash]
|
|
176
249
|
def parse_form_data(body, headers)
|
|
177
250
|
case (content_type = headers['content-type'])
|
|
178
251
|
when /^multipart\/form\-data; boundary=([^\s]+)/
|
|
@@ -185,6 +258,11 @@ module Syntropy
|
|
|
185
258
|
end
|
|
186
259
|
end
|
|
187
260
|
|
|
261
|
+
# Parses a multipart form body.
|
|
262
|
+
#
|
|
263
|
+
# @param body [String]
|
|
264
|
+
# @param boundary [String]
|
|
265
|
+
# @return [Hash]
|
|
188
266
|
def parse_multipart_form_data(body, boundary)
|
|
189
267
|
parts = body.split(boundary)
|
|
190
268
|
raise BadRequestError, 'Invalid form data' if parts.size < 2
|
|
@@ -197,6 +275,11 @@ module Syntropy
|
|
|
197
275
|
end
|
|
198
276
|
end
|
|
199
277
|
|
|
278
|
+
# Parses a multipart form data part.
|
|
279
|
+
#
|
|
280
|
+
# @param body [String]
|
|
281
|
+
# @param hash [Hash] output hash
|
|
282
|
+
# @return [void]
|
|
200
283
|
def parse_multipart_form_data_part(part, hash)
|
|
201
284
|
body, headers = parse_multipart_form_data_part_headers(part)
|
|
202
285
|
disposition = headers['content-disposition'] || ''
|
|
@@ -211,6 +294,10 @@ module Syntropy
|
|
|
211
294
|
end
|
|
212
295
|
end
|
|
213
296
|
|
|
297
|
+
# Parses a multipart form data part headers.
|
|
298
|
+
#
|
|
299
|
+
# @param part [String]
|
|
300
|
+
# @return [Hash]
|
|
214
301
|
def parse_multipart_form_data_part_headers(part)
|
|
215
302
|
headers = {}
|
|
216
303
|
while true
|
|
@@ -234,6 +321,10 @@ module Syntropy
|
|
|
234
321
|
MAX_PARAMETER_NAME_SIZE = 256
|
|
235
322
|
MAX_PARAMETER_VALUE_SIZE = 2**20 # 1MB
|
|
236
323
|
|
|
324
|
+
# Parses a URL-encoded form.
|
|
325
|
+
#
|
|
326
|
+
# @param body [String]
|
|
327
|
+
# @return [Hash]
|
|
237
328
|
def parse_urlencoded_form_data(body)
|
|
238
329
|
return {} unless body
|
|
239
330
|
|
|
@@ -7,18 +7,7 @@ require_relative '../http/status'
|
|
|
7
7
|
require_relative '../mime_types'
|
|
8
8
|
|
|
9
9
|
module Syntropy
|
|
10
|
-
|
|
11
|
-
class << self
|
|
12
|
-
def file_stat_to_etag(stat)
|
|
13
|
-
"#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def file_stat_to_last_modified(stat)
|
|
17
|
-
stat.mtime.httpdate
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
10
|
+
# Response methods.
|
|
22
11
|
module ResponseMethods
|
|
23
12
|
WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
24
13
|
|
data/lib/syntropy/request.rb
CHANGED
|
@@ -3,20 +3,26 @@
|
|
|
3
3
|
require_relative './request/request_info'
|
|
4
4
|
require_relative './request/validation'
|
|
5
5
|
require_relative './request/response'
|
|
6
|
-
require_relative './
|
|
6
|
+
require_relative './session'
|
|
7
7
|
require_relative './http/status'
|
|
8
8
|
|
|
9
9
|
module Syntropy
|
|
10
|
+
# Syntropy::Request represents an HTTP request. By interacting with the
|
|
11
|
+
# request, the app can extract request information and respond to the request.
|
|
10
12
|
class Request
|
|
11
13
|
include RequestInfoMethods
|
|
12
14
|
include RequestValidationMethods
|
|
13
15
|
include ResponseMethods
|
|
14
|
-
|
|
15
16
|
extend RequestInfoClassMethods
|
|
16
17
|
|
|
17
18
|
attr_reader :headers, :adapter, :start_stamp, :route_params
|
|
18
19
|
attr_accessor :route
|
|
19
20
|
|
|
21
|
+
# Initializes the request.
|
|
22
|
+
#
|
|
23
|
+
# @param headers [Hash] request headers
|
|
24
|
+
# @param adapter [Object] connection adapter
|
|
25
|
+
# @return [void]
|
|
20
26
|
def initialize(headers, adapter)
|
|
21
27
|
@headers = headers
|
|
22
28
|
@adapter = adapter
|
|
@@ -26,37 +32,62 @@ module Syntropy
|
|
|
26
32
|
@ctx = nil
|
|
27
33
|
end
|
|
28
34
|
|
|
29
|
-
# Returns the request context
|
|
35
|
+
# Returns the request context, used to store auxiliary information.
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash] request context hash
|
|
30
38
|
def ctx
|
|
31
39
|
@ctx ||= {}
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
# Returns the next request body chunk.
|
|
43
|
+
#
|
|
44
|
+
# @return [String, nil]
|
|
34
45
|
def next_chunk
|
|
35
46
|
@adapter.get_body_chunk(self)
|
|
36
47
|
end
|
|
37
48
|
|
|
49
|
+
# Reads request body chunks until the entire body is consumed, yielding each
|
|
50
|
+
# chunk to the given block.
|
|
51
|
+
#
|
|
52
|
+
# @return [void]
|
|
38
53
|
def each_chunk
|
|
39
54
|
while (chunk = @adapter.get_body_chunk(self))
|
|
40
55
|
yield chunk
|
|
41
56
|
end
|
|
42
57
|
end
|
|
43
58
|
|
|
59
|
+
# Reads the request body.
|
|
60
|
+
#
|
|
61
|
+
# @return [String, nil] request body
|
|
44
62
|
def read
|
|
45
63
|
@adapter.get_body(self)
|
|
46
64
|
end
|
|
47
65
|
alias_method :body, :read
|
|
48
66
|
|
|
67
|
+
# Returns true if the request body has been consumed.
|
|
68
|
+
#
|
|
69
|
+
# @return [bool]
|
|
49
70
|
def complete?
|
|
50
71
|
@adapter.complete?(self)
|
|
51
72
|
end
|
|
52
73
|
|
|
53
74
|
EMPTY_HEADERS = {}.freeze
|
|
54
75
|
|
|
76
|
+
# Sends a response.
|
|
77
|
+
#
|
|
78
|
+
# @param body [String, nil] response body
|
|
79
|
+
# @param headers [Hash] response headers
|
|
80
|
+
# @return [void]
|
|
55
81
|
def respond(body, headers = EMPTY_HEADERS)
|
|
56
82
|
@adapter.respond(self, body, headers)
|
|
57
83
|
@headers_sent = true
|
|
58
84
|
end
|
|
59
85
|
|
|
86
|
+
# Sends response headers.
|
|
87
|
+
#
|
|
88
|
+
# @param headers [Hash] response headers
|
|
89
|
+
# @param empty_response [bool] body should be sent
|
|
90
|
+
# @return [void]
|
|
60
91
|
def send_headers(headers = EMPTY_HEADERS, empty_response = false)
|
|
61
92
|
return if @headers_sent
|
|
62
93
|
|
|
@@ -64,6 +95,11 @@ module Syntropy
|
|
|
64
95
|
@adapter.send_headers(self, headers, empty_response: empty_response)
|
|
65
96
|
end
|
|
66
97
|
|
|
98
|
+
# Sends a response body chunk.
|
|
99
|
+
#
|
|
100
|
+
# @param body [String] response body chunk
|
|
101
|
+
# @param done [bool] body is complete
|
|
102
|
+
# @return [void]
|
|
67
103
|
def send_chunk(body, done: false)
|
|
68
104
|
send_headers({}) unless @headers_sent
|
|
69
105
|
|
|
@@ -71,36 +107,32 @@ module Syntropy
|
|
|
71
107
|
end
|
|
72
108
|
alias_method :<<, :send_chunk
|
|
73
109
|
|
|
110
|
+
# Finish response.
|
|
111
|
+
#
|
|
112
|
+
# @return [void]
|
|
74
113
|
def finish
|
|
75
114
|
send_headers({}) unless @headers_sent
|
|
76
115
|
|
|
77
116
|
@adapter.finish(self)
|
|
78
117
|
end
|
|
79
118
|
|
|
119
|
+
# Returns true if response headers were sent.
|
|
120
|
+
#
|
|
121
|
+
# @return [bool]
|
|
80
122
|
def headers_sent?
|
|
81
123
|
@headers_sent
|
|
82
124
|
end
|
|
83
125
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def tx_incr(count)
|
|
89
|
-
headers[':tx'] ? headers[':tx'] += count : headers[':tx'] = count
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def transfer_counts
|
|
93
|
-
[headers[':rx'], headers[':tx']]
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def total_transfer
|
|
97
|
-
(headers[':rx'] || 0) + (headers[':tx'] || 0)
|
|
98
|
-
end
|
|
99
|
-
|
|
126
|
+
# Returns the request session.
|
|
127
|
+
#
|
|
128
|
+
# @return [Syntropy::Session]
|
|
100
129
|
def session
|
|
101
130
|
@session ||= Session.new(self)
|
|
102
131
|
end
|
|
103
132
|
|
|
133
|
+
# Returns the request flash session storage.
|
|
134
|
+
#
|
|
135
|
+
# @return [Syntropy::Session::Flash]
|
|
104
136
|
def flash
|
|
105
137
|
session.flash
|
|
106
138
|
end
|