syntropy 0.32.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 +14 -0
- data/TODO.md +0 -39
- data/cmd/console.rb +18 -7
- data/cmd/serve.rb +26 -20
- data/cmd/test.rb +90 -21
- 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 +8 -5
- data/examples/blog/app/posts/index.rb +7 -5
- 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/examples/mcp-oauth/test/test_app.rb +2 -20
- data/examples/mcp-oauth/test/test_oauth.rb +93 -217
- 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 +15 -13
- 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 +25 -13
- 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 +3 -14
- data/lib/syntropy/request/validation.rb +1 -0
- data/lib/syntropy/request.rb +55 -14
- 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 +168 -2
- data/lib/syntropy/utils.rb +53 -18
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +44 -10
- data/syntropy.gemspec +1 -0
- 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 +11 -8
- data/test/test_module_loader.rb +5 -2
- data/test/test_request_session.rb +254 -0
- 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 +67 -42
- data/examples/blog/app/_setup.rb +0 -4
- data/examples/mcp-oauth/test/helper.rb +0 -9
- /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
|
@@ -36,7 +36,7 @@ rescue Timeout::Error
|
|
|
36
36
|
req.send_chunk("retry: 0\n\n", done: true) rescue nil
|
|
37
37
|
rescue SystemCallError
|
|
38
38
|
# ignore
|
|
39
|
-
rescue => e
|
|
39
|
+
rescue StandardError => e
|
|
40
40
|
@logger&.error(
|
|
41
41
|
message: 'Unexpected error encountered while serving auto refresh watcher',
|
|
42
42
|
error: e
|
data/lib/syntropy/db/schema.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Syntropy
|
|
|
53
53
|
connection_pool.with_db do |db|
|
|
54
54
|
current_version = get_schema_version(db)
|
|
55
55
|
migrations_keys = @migrations.keys.sort
|
|
56
|
-
migrations_keys.select { it > current_version } if current_version
|
|
56
|
+
migrations_keys.select! { it > current_version } if current_version
|
|
57
57
|
|
|
58
58
|
migrations_keys.each do |key|
|
|
59
59
|
db.transaction do
|
data/lib/syntropy/db/store.rb
CHANGED
data/lib/syntropy/errors.rb
CHANGED
|
@@ -43,8 +43,6 @@ module Syntropy
|
|
|
43
43
|
# @return [Syntropy::Error]
|
|
44
44
|
def self.teapot(msg = 'I\'m a teapot') = new(msg, HTTP::TEAPOT)
|
|
45
45
|
|
|
46
|
-
attr_reader :http_status
|
|
47
|
-
|
|
48
46
|
# Initializes a Syntropy error with the given HTTP status and message.
|
|
49
47
|
#
|
|
50
48
|
# @param http_status [Integer, String] HTTP status
|
|
@@ -70,21 +68,27 @@ module Syntropy
|
|
|
70
68
|
end
|
|
71
69
|
end
|
|
72
70
|
|
|
71
|
+
# ProtocolError is raised when an HTTP protocol error is encountered.
|
|
73
72
|
class ProtocolError < Error
|
|
74
73
|
def http_status
|
|
75
74
|
HTTP::BAD_REQUEST
|
|
76
75
|
end
|
|
77
76
|
end
|
|
78
77
|
|
|
78
|
+
# UnsupportedHTTPVersionError is raised when an invalid protocol specified in
|
|
79
|
+
# a request's request line.
|
|
79
80
|
class UnsupportedHTTPVersionError < ProtocolError
|
|
80
81
|
def http_status
|
|
81
82
|
HTTP::HTTP_VERSION_NOT_SUPPORTED
|
|
82
83
|
end
|
|
83
84
|
end
|
|
84
85
|
|
|
86
|
+
# BadRequestError is raised when a request contains invalid information.
|
|
85
87
|
class BadRequestError < Error
|
|
86
88
|
end
|
|
87
89
|
|
|
90
|
+
# InvalidRequestContentTypeError is raised when a request has an invalid
|
|
91
|
+
# content type.
|
|
88
92
|
class InvalidRequestContentTypeError < Error
|
|
89
93
|
def http_status
|
|
90
94
|
HTTP::UNSUPPORTED_MEDIA_TYPE
|
data/lib/syntropy/http/client.rb
CHANGED
|
@@ -23,12 +23,11 @@ module Syntropy
|
|
|
23
23
|
|
|
24
24
|
@done = nil
|
|
25
25
|
@response_headers = nil
|
|
26
|
+
@response_cookies = nil
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def run
|
|
29
30
|
loop do
|
|
30
|
-
@done = nil
|
|
31
|
-
@response_headers = nil
|
|
32
31
|
persist = serve_request
|
|
33
32
|
break if !persist
|
|
34
33
|
end
|
|
@@ -47,6 +46,9 @@ module Syntropy
|
|
|
47
46
|
# object and handing it off to the app handler. Returns true if the
|
|
48
47
|
# connection should be persisted.
|
|
49
48
|
def serve_request
|
|
49
|
+
@done = nil
|
|
50
|
+
@response_headers = nil
|
|
51
|
+
@response_cookies = nil
|
|
50
52
|
@closed = nil
|
|
51
53
|
headers = @io.http_read_request_headers
|
|
52
54
|
return false if !headers
|
|
@@ -130,13 +132,10 @@ module Syntropy
|
|
|
130
132
|
@response_headers ? @response_headers.merge!(headers) : @response_headers = headers
|
|
131
133
|
end
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
else
|
|
138
|
-
set_response_headers('Set-Cookie' => cookies)
|
|
139
|
-
end
|
|
135
|
+
DELETE_COOKIE = "; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly"
|
|
136
|
+
|
|
137
|
+
def set_cookie(key, value)
|
|
138
|
+
(@response_cookies ||= {})[key] = value || DELETE_COOKIE
|
|
140
139
|
end
|
|
141
140
|
|
|
142
141
|
SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
|
|
@@ -152,11 +151,11 @@ module Syntropy
|
|
|
152
151
|
# @param body [String] response body
|
|
153
152
|
# @param headers
|
|
154
153
|
def respond(request, body, headers)
|
|
154
|
+
add_set_cookie_headers if @response_cookies
|
|
155
155
|
headers = @response_headers.merge(headers) if @response_headers
|
|
156
156
|
|
|
157
157
|
formatted_headers = format_headers(headers, body)
|
|
158
158
|
@response_headers = headers
|
|
159
|
-
request&.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
|
|
160
159
|
if body
|
|
161
160
|
chunk_prelude = "#{body.bytesize.to_s(16)}\r\n"
|
|
162
161
|
@machine.sendv(@fd, formatted_headers, chunk_prelude, body, CHUNKED_ENCODING_POSTLUDE)
|
|
@@ -175,7 +174,6 @@ module Syntropy
|
|
|
175
174
|
# @return [void]
|
|
176
175
|
def send_headers(request, headers, empty_response: false)
|
|
177
176
|
formatted_headers = format_headers(headers, !empty_response)
|
|
178
|
-
request.tx_incr(formatted_headers.bytesize)
|
|
179
177
|
@machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
|
|
180
178
|
@response_headers = headers
|
|
181
179
|
end
|
|
@@ -193,7 +191,6 @@ module Syntropy
|
|
|
193
191
|
data << EMPTY_CHUNK if done
|
|
194
192
|
return if data.empty?
|
|
195
193
|
|
|
196
|
-
request.tx_incr(data.bytesize)
|
|
197
194
|
@machine.send(@fd, data, data.bytesize, SEND_FLAGS)
|
|
198
195
|
return if @done || !done
|
|
199
196
|
|
|
@@ -205,7 +202,6 @@ module Syntropy
|
|
|
205
202
|
# default headers are sent using #send_headers.
|
|
206
203
|
# @return [void]
|
|
207
204
|
def finish(request)
|
|
208
|
-
request.tx_incr(EMPTY_CHUNK_LEN)
|
|
209
205
|
@machine.send(@fd, EMPTY_CHUNK, EMPTY_CHUNK_LEN, SEND_FLAGS)
|
|
210
206
|
return if @done
|
|
211
207
|
|
|
@@ -315,6 +311,12 @@ module Syntropy
|
|
|
315
311
|
lines << "#{key}: #{value}\r\n"
|
|
316
312
|
end
|
|
317
313
|
end
|
|
314
|
+
|
|
315
|
+
def add_set_cookie_headers
|
|
316
|
+
@response_headers ||= {}
|
|
317
|
+
sc = (@response_headers['Set-Cookie'] ||= [])
|
|
318
|
+
@response_cookies.each { |k, v| sc << "#{k}=#{v}" }
|
|
319
|
+
end
|
|
318
320
|
end
|
|
319
321
|
end
|
|
320
322
|
end
|
data/lib/syntropy/json_api.rb
CHANGED
|
@@ -4,11 +4,20 @@ require 'syntropy/errors'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
|
|
6
6
|
module Syntropy
|
|
7
|
+
# JSONAPI is a controller that implements a JSON API.
|
|
7
8
|
class JSONAPI
|
|
9
|
+
# Initializes the controller.
|
|
10
|
+
#
|
|
11
|
+
# @param env [Hash] app environment
|
|
12
|
+
# @return [void]
|
|
8
13
|
def initialize(env)
|
|
9
14
|
@env = env
|
|
10
15
|
end
|
|
11
16
|
|
|
17
|
+
# Processes the given request.
|
|
18
|
+
#
|
|
19
|
+
# @param req [Syntropy::Request]
|
|
20
|
+
# @return [void]
|
|
12
21
|
def call(req)
|
|
13
22
|
response, status = __invoke__(req)
|
|
14
23
|
req.respond(
|
|
@@ -20,6 +29,9 @@ module Syntropy
|
|
|
20
29
|
|
|
21
30
|
private
|
|
22
31
|
|
|
32
|
+
# Processes the request by invoking the corresponding object method.
|
|
33
|
+
#
|
|
34
|
+
# @param req [Syntropy::Request]
|
|
23
35
|
def __invoke__(req)
|
|
24
36
|
q = req.validate_param(:q, String).to_sym
|
|
25
37
|
response = case req.method
|
|
@@ -31,7 +43,7 @@ module Syntropy
|
|
|
31
43
|
raise Syntropy::Error.method_not_allowed
|
|
32
44
|
end
|
|
33
45
|
[{ status: 'OK', response: response }, HTTP::OK]
|
|
34
|
-
rescue => e
|
|
46
|
+
rescue StandardError => e
|
|
35
47
|
if !e.is_a?(Syntropy::Error)
|
|
36
48
|
p e
|
|
37
49
|
p e.backtrace
|
|
@@ -39,6 +51,11 @@ module Syntropy
|
|
|
39
51
|
__error_response__(e)
|
|
40
52
|
end
|
|
41
53
|
|
|
54
|
+
# Processes a GET request.
|
|
55
|
+
#
|
|
56
|
+
# @param sym [Symbol] object method
|
|
57
|
+
# @param req [Syntropy::Request] request
|
|
58
|
+
# @return [any] method call return value
|
|
42
59
|
def __invoke_get__(sym, req)
|
|
43
60
|
return send(sym, req) if respond_to?(sym)
|
|
44
61
|
|
|
@@ -46,6 +63,11 @@ module Syntropy
|
|
|
46
63
|
raise err
|
|
47
64
|
end
|
|
48
65
|
|
|
66
|
+
# Processes a POST request.
|
|
67
|
+
#
|
|
68
|
+
# @param sym [Symbol] object method
|
|
69
|
+
# @param req [Syntropy::Request] request
|
|
70
|
+
# @return [any] method call return value
|
|
49
71
|
def __invoke_post__(sym, req)
|
|
50
72
|
sym_post = :"#{sym}!"
|
|
51
73
|
return send(sym_post, req) if respond_to?(sym_post)
|
|
@@ -54,6 +76,10 @@ module Syntropy
|
|
|
54
76
|
raise err
|
|
55
77
|
end
|
|
56
78
|
|
|
79
|
+
# Generates an error response in case of exception.
|
|
80
|
+
#
|
|
81
|
+
# @param err [Exception] raised Exception
|
|
82
|
+
# @return [Hash] error response
|
|
57
83
|
def __error_response__(err)
|
|
58
84
|
http_status = err.respond_to?(:http_status) ? err.http_status : HTTP::INTERNAL_SERVER_ERROR
|
|
59
85
|
error_name = err.class.name.split('::').last
|
data/lib/syntropy/logger.rb
CHANGED
|
@@ -3,29 +3,51 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module Syntropy
|
|
6
|
+
# The Logger class implements a logger with support for structured logging.
|
|
6
7
|
class Logger
|
|
8
|
+
# Initializes the logger.
|
|
9
|
+
#
|
|
10
|
+
# @param machine [UringMachine] machine instance
|
|
11
|
+
# @param fd [Integer] file descriptor for writing log messages
|
|
12
|
+
# @param opts [Hash] logger options
|
|
13
|
+
# @return [void]
|
|
7
14
|
def initialize(machine, fd = $stdout.fileno, **opts)
|
|
8
15
|
@machine = machine
|
|
9
16
|
@fd = fd
|
|
10
17
|
@opts = opts
|
|
11
18
|
end
|
|
12
19
|
|
|
20
|
+
# Logs an INFO entry.
|
|
21
|
+
#
|
|
22
|
+
# @param o [Hash] log entry
|
|
23
|
+
# @return [void]
|
|
13
24
|
def info(o)
|
|
14
25
|
call(:INFO, o)
|
|
15
26
|
end
|
|
16
27
|
|
|
28
|
+
# Logs an WARN entry.
|
|
29
|
+
#
|
|
30
|
+
# @param o [Hash] log entry
|
|
31
|
+
# @return [void]
|
|
17
32
|
def warn(o)
|
|
18
33
|
call(:WARN, o)
|
|
19
34
|
end
|
|
20
35
|
|
|
36
|
+
# Logs an ERROR entry.
|
|
37
|
+
#
|
|
38
|
+
# @param o [Hash] log entry
|
|
39
|
+
# @return [void]
|
|
21
40
|
def error(o)
|
|
22
41
|
call(:ERROR, o)
|
|
23
42
|
end
|
|
24
43
|
|
|
25
44
|
private
|
|
26
45
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
46
|
+
# Writes a log entry.
|
|
47
|
+
#
|
|
48
|
+
# @param level [Symbol] log level
|
|
49
|
+
# @param o [Hash] entry
|
|
50
|
+
# @return [void]
|
|
29
51
|
def call(level, o)
|
|
30
52
|
emit(make_entry(level, o))
|
|
31
53
|
rescue StandardError => e
|
|
@@ -35,10 +57,20 @@ module Syntropy
|
|
|
35
57
|
exit
|
|
36
58
|
end
|
|
37
59
|
|
|
60
|
+
# Emits an entry to the associated output.
|
|
61
|
+
#
|
|
62
|
+
# @param entry [Hash] log entry
|
|
63
|
+
# @return [void]
|
|
38
64
|
def emit(entry)
|
|
39
65
|
@machine.write_async(@fd, "#{entry.to_json}\n")
|
|
40
66
|
end
|
|
41
67
|
|
|
68
|
+
# Transforms raw entry into a log entry. Additional information is added
|
|
69
|
+
# dependending on the kind of entry.
|
|
70
|
+
#
|
|
71
|
+
# @param level [Symbol] log level
|
|
72
|
+
# @param o [Hash] raw entry
|
|
73
|
+
# @return [Hash] log entry
|
|
42
74
|
def make_entry(level, o)
|
|
43
75
|
if o[:request]
|
|
44
76
|
make_request_entry(level, o)
|
|
@@ -49,52 +81,74 @@ module Syntropy
|
|
|
49
81
|
end
|
|
50
82
|
end
|
|
51
83
|
|
|
84
|
+
# Makes an error log entry.
|
|
85
|
+
#
|
|
86
|
+
# @param level [Symbol] log level
|
|
87
|
+
# @param o [Hash] input entry
|
|
88
|
+
# @return [Hash] output entry
|
|
52
89
|
def make_error_entry(level, o)
|
|
53
90
|
err = o[:error]
|
|
91
|
+
t = Time.now
|
|
54
92
|
{
|
|
55
|
-
level:
|
|
56
|
-
ts:
|
|
57
|
-
ts_s:
|
|
58
|
-
}
|
|
59
|
-
.merge(o)
|
|
60
|
-
.merge(
|
|
93
|
+
level: level.to_s,
|
|
94
|
+
ts: t.to_i,
|
|
95
|
+
ts_s: t.iso8601
|
|
96
|
+
}.merge(o).merge(
|
|
61
97
|
error: "#{err.class}: #{err.message}",
|
|
62
98
|
backtrace: err.backtrace
|
|
63
99
|
)
|
|
64
100
|
end
|
|
65
101
|
|
|
102
|
+
# Makes a request log entry.
|
|
103
|
+
#
|
|
104
|
+
# @param level [Symbol] log level
|
|
105
|
+
# @param o [Hash] input entry
|
|
106
|
+
# @return [Hash] output entry
|
|
66
107
|
def make_request_entry(level, o)
|
|
67
108
|
request = o[:request]
|
|
68
109
|
request_headers = request.headers
|
|
69
110
|
response_headers = o[:response_headers]
|
|
70
111
|
elapsed = monotonic_clock - request.start_stamp
|
|
112
|
+
t = Time.now
|
|
71
113
|
{
|
|
72
|
-
level:
|
|
73
|
-
ts:
|
|
74
|
-
ts_s:
|
|
75
|
-
message:
|
|
76
|
-
client_ip:
|
|
77
|
-
http_method:
|
|
78
|
-
user_agent:
|
|
79
|
-
uri:
|
|
80
|
-
status:
|
|
81
|
-
elapsed:
|
|
114
|
+
level: level.to_s,
|
|
115
|
+
ts: t.to_i,
|
|
116
|
+
ts_s: t.iso8601,
|
|
117
|
+
message: o[:message] || 'HTTP request done',
|
|
118
|
+
client_ip: request.forwarded_for || '?',
|
|
119
|
+
http_method: request_headers[':method'].upcase,
|
|
120
|
+
user_agent: request_headers['user-agent'],
|
|
121
|
+
uri: full_uri(request_headers),
|
|
122
|
+
status: response_headers[':status'] || '200',
|
|
123
|
+
elapsed: elapsed
|
|
82
124
|
}
|
|
83
125
|
end
|
|
84
126
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
# Makes a request log entry.
|
|
128
|
+
#
|
|
129
|
+
# @param level [Symbol] log level
|
|
130
|
+
# @param o [Hash] input entry
|
|
131
|
+
# @return [Hash] output entry
|
|
89
132
|
def make_hash_entry(level, hash)
|
|
133
|
+
t = Time.now
|
|
90
134
|
{
|
|
91
|
-
level:
|
|
92
|
-
ts:
|
|
93
|
-
ts_s:
|
|
94
|
-
}
|
|
95
|
-
|
|
135
|
+
level: level.to_s,
|
|
136
|
+
ts: t.to_i,
|
|
137
|
+
ts_s: t.iso8601
|
|
138
|
+
}.merge(hash)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns the monotonic clock.
|
|
142
|
+
#
|
|
143
|
+
# @return [Float]
|
|
144
|
+
def monotonic_clock
|
|
145
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
96
146
|
end
|
|
97
147
|
|
|
148
|
+
# Returns the full request URI for the given request headers.
|
|
149
|
+
#
|
|
150
|
+
# @param headers [Hash] request headers
|
|
151
|
+
# @return [String] request URI
|
|
98
152
|
def full_uri(headers)
|
|
99
153
|
format(
|
|
100
154
|
'%<scheme>s://%<host>s%<path>s',
|
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
|
|
@@ -39,11 +39,12 @@ module Syntropy
|
|
|
39
39
|
# @return [any] export value
|
|
40
40
|
def load(ref, raise_on_missing: true)
|
|
41
41
|
ref = "/#{ref}" if ref !~ /^\//
|
|
42
|
+
if !(entry = @modules[ref])
|
|
43
|
+
entry = load_module(ref, raise_on_missing:)
|
|
44
|
+
return if !entry
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@modules[ref] ||= entry
|
|
46
|
+
@modules[ref] = entry
|
|
47
|
+
end
|
|
47
48
|
entry[:export_value]
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -54,8 +55,8 @@ module Syntropy
|
|
|
54
55
|
# @param dir [String] relative module directory
|
|
55
56
|
# @return [Array<String>] list of modules
|
|
56
57
|
def list(dir)
|
|
57
|
-
fns = Dir[File.join(@
|
|
58
|
-
fns.map { it.match(/^#{@
|
|
58
|
+
fns = Dir[File.join(@app_root, dir, '*.rb')]
|
|
59
|
+
fns.map { it.match(/^#{@app_root}\/(.+)\.rb$/)[1] }.sort
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# Invalidates a module by its filename, normally following a change to the
|
|
@@ -88,10 +89,13 @@ module Syntropy
|
|
|
88
89
|
entry[:reverse_deps].each { invalidate_ref(it) }
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
# Invalidates a collection module.
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
91
95
|
def invalidate_collection_modules
|
|
92
96
|
refs = []
|
|
93
97
|
@modules.each do |ref, entry|
|
|
94
|
-
refs << ref if entry[:module].
|
|
98
|
+
refs << ref if entry[:module].collection_module?
|
|
95
99
|
end
|
|
96
100
|
refs.each { invalidate_ref(it) }
|
|
97
101
|
end
|
|
@@ -117,7 +121,7 @@ module Syntropy
|
|
|
117
121
|
# @return [Hash] module entry
|
|
118
122
|
def load_module(ref, raise_on_missing: true)
|
|
119
123
|
ref = "/#{ref}" if ref !~ /^\//
|
|
120
|
-
fn = File.expand_path(File.join(@
|
|
124
|
+
fn = File.expand_path(File.join(@app_root, "#{ref}.rb"))
|
|
121
125
|
if !File.file?(fn)
|
|
122
126
|
raise Syntropy::Error, "File not found #{fn}" if raise_on_missing
|
|
123
127
|
|
|
@@ -141,6 +145,10 @@ module Syntropy
|
|
|
141
145
|
}
|
|
142
146
|
end
|
|
143
147
|
|
|
148
|
+
# Cleans up a module reference specifier, turning /index into /
|
|
149
|
+
#
|
|
150
|
+
# @param ref [String] input ref
|
|
151
|
+
# @return [String] clean ref
|
|
144
152
|
def clean_ref(ref)
|
|
145
153
|
return '/' if ref =~ /^index(\+)?$/
|
|
146
154
|
|
|
@@ -221,7 +229,7 @@ module Syntropy
|
|
|
221
229
|
# #collection_module!
|
|
222
230
|
#
|
|
223
231
|
# @return [bool]
|
|
224
|
-
def
|
|
232
|
+
def collection_module?
|
|
225
233
|
@collection_module_p
|
|
226
234
|
end
|
|
227
235
|
|
|
@@ -257,6 +265,10 @@ module Syntropy
|
|
|
257
265
|
self
|
|
258
266
|
end
|
|
259
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
|
|
260
272
|
def normalize_import_ref(ref)
|
|
261
273
|
base = @ref == '' ? '/' : @ref
|
|
262
274
|
if ref =~ /^\//
|
|
@@ -273,7 +285,7 @@ module Syntropy
|
|
|
273
285
|
# @return [Papercraft::Template] template
|
|
274
286
|
def template(proc = nil, &block)
|
|
275
287
|
proc ||= block
|
|
276
|
-
raise
|
|
288
|
+
raise 'No template block/proc given' if !proc
|
|
277
289
|
|
|
278
290
|
Papercraft::Template.new(proc)
|
|
279
291
|
end
|
|
@@ -285,10 +297,10 @@ module Syntropy
|
|
|
285
297
|
# @return [Papercraft::Template] template
|
|
286
298
|
def template_xml(proc = nil, &block)
|
|
287
299
|
proc ||= block
|
|
288
|
-
raise
|
|
300
|
+
raise 'No template block/proc given' if !proc
|
|
289
301
|
|
|
290
302
|
Papercraft::Template.new(proc, mode: :xml)
|
|
291
|
-
rescue => e
|
|
303
|
+
rescue StandardError => e
|
|
292
304
|
p e
|
|
293
305
|
p e.backtrace
|
|
294
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
|
}
|