syntropy 0.33.0 → 0.34.1
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 +11 -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/_layout/default.rb +3 -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/assets/style.css +20 -0
- data/examples/blog/app/index.rb +12 -2
- 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/app/test.rb +7 -0
- 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/examples/template/.gitignore +2 -0
- data/examples/template/Gemfile +3 -0
- data/examples/template/app/_layout/default.rb +14 -0
- data/examples/template/app/_lib/database.rb +13 -0
- data/examples/template/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/template/app/assets/style.css +25 -0
- data/examples/template/app/index.rb +27 -0
- data/examples/template/app/test.rb +7 -0
- data/examples/template/config/development.rb +5 -0
- data/examples/template/config/production.rb +4 -0
- data/examples/template/config/test.rb +5 -0
- data/examples/template/test/test_app.rb +14 -0
- data/lib/syntropy/app.rb +48 -40
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
- data/lib/syntropy/applets/builtin/default_error_handler/style.css +4 -8
- data/lib/syntropy/applets/builtin/default_error_handler.rb +18 -9
- 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 +31 -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 +66 -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
data/lib/syntropy/markdown.rb
CHANGED
|
@@ -3,39 +3,68 @@
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
|
|
5
5
|
module Syntropy
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Parses the markdown file at the given path.
|
|
14
|
-
#
|
|
15
|
-
# @param path [String] file path
|
|
16
|
-
# @return [Array] an tuple containing properties<Hash>, contents<String>
|
|
17
|
-
def self.parse_markdown_file(path, env)
|
|
18
|
-
content = IO.read(path) || ''
|
|
19
|
-
atts = {}
|
|
20
|
-
|
|
21
|
-
# Parse date from file name
|
|
22
|
-
m = path.match(DATE_REGEXP)
|
|
23
|
-
atts[:date] ||= Date.parse(m[1]) if m
|
|
24
|
-
|
|
25
|
-
if (m = content.match(FRONT_MATTER_REGEXP))
|
|
26
|
-
front_matter = m[1]
|
|
27
|
-
content = m.post_match
|
|
28
|
-
|
|
29
|
-
yaml = YAML.safe_load(front_matter, **YAML_OPTS)
|
|
30
|
-
atts = atts.merge(yaml)
|
|
31
|
-
end
|
|
6
|
+
# Markdown parsing.
|
|
7
|
+
module Markdown
|
|
8
|
+
FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
|
|
9
|
+
YAML_OPTS = {
|
|
10
|
+
permitted_classes: [Date],
|
|
11
|
+
symbolize_names: true
|
|
12
|
+
}.freeze
|
|
32
13
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
class << self
|
|
15
|
+
# Parses the markdown file at the given path.
|
|
16
|
+
#
|
|
17
|
+
# @param path [String] file path
|
|
18
|
+
# @return [Array] an tuple containing properties<Hash>, contents<String>
|
|
19
|
+
def parse(path, env)
|
|
20
|
+
content = IO.read(path) || ''
|
|
21
|
+
atts = {}
|
|
22
|
+
|
|
23
|
+
parse_date(path, atts)
|
|
24
|
+
content = parse_content(content, atts)
|
|
25
|
+
atts[:url] = path_to_url(path, env[:app_root]) if env[:app_root]
|
|
26
|
+
|
|
27
|
+
[atts, content]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
# Parses date information from the given path.
|
|
33
|
+
#
|
|
34
|
+
# @param path [String] file path
|
|
35
|
+
# @param atts [Hash] file attributes
|
|
36
|
+
# @return [void]
|
|
37
|
+
def parse_date(path, atts)
|
|
38
|
+
# Parse date from file name
|
|
39
|
+
if (m = path.match(/(\d{4}-\d{2}-\d{2})/))
|
|
40
|
+
atts[:date] ||= Date.parse(m[1])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parses the markdown content and front matter attributes from the given content.
|
|
45
|
+
#
|
|
46
|
+
# @param content [String] file content
|
|
47
|
+
# @param atts [Hash] file attributes
|
|
48
|
+
# @return [String] parsed markdown content
|
|
49
|
+
def parse_content(content, atts)
|
|
50
|
+
if (m = content.match(FRONT_MATTER_REGEXP))
|
|
51
|
+
front_matter = m[1]
|
|
52
|
+
content = m.post_match
|
|
53
|
+
|
|
54
|
+
yaml = YAML.safe_load(front_matter, **YAML_OPTS)
|
|
55
|
+
atts.merge!(yaml)
|
|
56
|
+
end
|
|
57
|
+
content
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Converts the markdown file path to URL
|
|
61
|
+
#
|
|
62
|
+
# @param path [String] file path
|
|
63
|
+
# @param app_root [String] app root directory
|
|
64
|
+
# @return [String] url
|
|
65
|
+
def path_to_url(path, app_root)
|
|
66
|
+
path.gsub(/#{app_root}/, '').gsub(/\.md$/, '')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
40
69
|
end
|
|
41
70
|
end
|
data/lib/syntropy/mime_types.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Syntropy
|
|
4
|
-
#
|
|
4
|
+
# The MimeTypes module maps file extensions to MIME types.
|
|
5
5
|
module MimeTypes
|
|
6
6
|
TYPES = {
|
|
7
7
|
'html' => 'text/html',
|
|
@@ -21,16 +21,20 @@ module Syntropy
|
|
|
21
21
|
|
|
22
22
|
EXT_REGEXP = /\.?([^\.]+)$/.freeze
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# Returns the mime type for the given file extension.
|
|
25
|
+
#
|
|
26
|
+
# @param ext [String, Symbol] file extension
|
|
27
|
+
# @return [String, nil] MIME type
|
|
28
|
+
def self.[](ext)
|
|
29
|
+
case ext
|
|
26
30
|
when Symbol
|
|
27
|
-
TYPES[
|
|
31
|
+
TYPES[ext.to_s]
|
|
28
32
|
when EXT_REGEXP
|
|
29
33
|
TYPES[Regexp.last_match(1)]
|
|
30
34
|
when ''
|
|
31
35
|
nil
|
|
32
36
|
else
|
|
33
|
-
raise "Invalid argument #{
|
|
37
|
+
raise "Invalid argument #{ext.inspect}"
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -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
|
|
|
@@ -191,6 +198,17 @@ module Syntropy
|
|
|
191
198
|
m.instance_eval(code, fn)
|
|
192
199
|
env[:logger]&.info(message: "Loaded module at #{fn}")
|
|
193
200
|
m
|
|
201
|
+
rescue SyntaxError => e
|
|
202
|
+
STDERR.puts("\n#{e.message}")
|
|
203
|
+
|
|
204
|
+
if (m = e.message.match(/^(.+)\: syntax/))
|
|
205
|
+
location = m[1]
|
|
206
|
+
e2 = SyntaxError.new("Syntax errors found in module #{env[:ref]}")
|
|
207
|
+
e2.set_backtrace([location] + e.backtrace)
|
|
208
|
+
raise e2
|
|
209
|
+
else
|
|
210
|
+
raise e
|
|
211
|
+
end
|
|
194
212
|
end
|
|
195
213
|
|
|
196
214
|
# Initializes a module with the given environment hash.
|
|
@@ -222,7 +240,7 @@ module Syntropy
|
|
|
222
240
|
# #collection_module!
|
|
223
241
|
#
|
|
224
242
|
# @return [bool]
|
|
225
|
-
def
|
|
243
|
+
def collection_module?
|
|
226
244
|
@collection_module_p
|
|
227
245
|
end
|
|
228
246
|
|
|
@@ -258,6 +276,10 @@ module Syntropy
|
|
|
258
276
|
self
|
|
259
277
|
end
|
|
260
278
|
|
|
279
|
+
# Normalize an import reference, turning a relative path into an absolute one.
|
|
280
|
+
#
|
|
281
|
+
# @param ref [String] input ref
|
|
282
|
+
# @return [String] normalized ref
|
|
261
283
|
def normalize_import_ref(ref)
|
|
262
284
|
base = @ref == '' ? '/' : @ref
|
|
263
285
|
if ref =~ /^\//
|
|
@@ -274,7 +296,7 @@ module Syntropy
|
|
|
274
296
|
# @return [Papercraft::Template] template
|
|
275
297
|
def template(proc = nil, &block)
|
|
276
298
|
proc ||= block
|
|
277
|
-
raise
|
|
299
|
+
raise 'No template block/proc given' if !proc
|
|
278
300
|
|
|
279
301
|
Papercraft::Template.new(proc)
|
|
280
302
|
end
|
|
@@ -286,10 +308,10 @@ module Syntropy
|
|
|
286
308
|
# @return [Papercraft::Template] template
|
|
287
309
|
def template_xml(proc = nil, &block)
|
|
288
310
|
proc ||= block
|
|
289
|
-
raise
|
|
311
|
+
raise 'No template block/proc given' if !proc
|
|
290
312
|
|
|
291
313
|
Papercraft::Template.new(proc, mode: :xml)
|
|
292
|
-
rescue => e
|
|
314
|
+
rescue StandardError => e
|
|
293
315
|
p e
|
|
294
316
|
p e.backtrace
|
|
295
317
|
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
|
|