syntropy 0.39.0 → 0.41.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 +8 -0
- data/TODO.md +44 -142
- data/cmd/console.rb +0 -2
- data/cmd/new/template/config/production.rb +2 -1
- data/cmd/new.rb +1 -1
- data/cmd/serve.rb +26 -0
- data/examples/agent/Dockerfile +25 -0
- data/examples/agent/Gemfile +3 -0
- data/examples/agent/README.md +40 -0
- data/examples/agent/TODO.md +5 -0
- data/examples/agent/app/_layout/default.rb +15 -0
- data/examples/agent/app/_lib/storage.rb +13 -0
- data/examples/agent/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/agent/app/assets/agent.js +63 -0
- data/examples/agent/app/assets/minigfm.js +207 -0
- data/examples/agent/app/assets/model.js +45 -0
- data/examples/agent/app/assets/style.css +25 -0
- data/examples/agent/app/assets/tools.js +74 -0
- data/examples/agent/app/assets/ui.js +50 -0
- data/examples/agent/app/index.rb +19 -0
- data/examples/agent/app/test.rb +7 -0
- data/examples/agent/config/Caddyfile +5 -0
- data/examples/agent/config/development.rb +5 -0
- data/examples/agent/config/production.rb +5 -0
- data/examples/agent/config/test.rb +5 -0
- data/examples/agent/docker-compose.yml +71 -0
- data/examples/agent/response_example.json +108 -0
- data/examples/agent/test/test_app.rb +14 -0
- data/examples/blog/app/foo.rb +1 -0
- data/examples/blog/config/production.rb +2 -1
- data/lib/syntropy/app.rb +6 -2
- data/lib/syntropy/applets/builtin/auto_refresh/watch.js +1 -1
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +7 -0
- data/lib/syntropy/dev_mode.rb +8 -3
- data/lib/syntropy/http/client.rb +17 -1
- data/lib/syntropy/http/client_connection.rb +16 -0
- data/lib/syntropy/http/io_extensions.rb +32 -4
- data/lib/syntropy/http/server.rb +55 -24
- data/lib/syntropy/http/server_connection.rb +75 -4
- data/lib/syntropy/logger.rb +8 -0
- data/lib/syntropy/markdown.rb +44 -20
- data/lib/syntropy/module_loader.rb +29 -16
- data/lib/syntropy/request/request_info.rb +1 -1
- data/lib/syntropy/request/response.rb +75 -6
- data/lib/syntropy/request/validation.rb +6 -0
- data/lib/syntropy/storage/connection_pool.rb +34 -0
- data/lib/syntropy/test.rb +4 -4
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +7 -1
- data/syntropy.gemspec +4 -4
- data/test/test_markdown.rb +3 -3
- data/test/test_module_loader.rb +4 -1
- metadata +34 -11
data/lib/syntropy/markdown.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Syntropy
|
|
|
12
12
|
symbolize_names: true
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
|
+
# Implements a Markdown file controller.
|
|
15
16
|
class Controller
|
|
16
17
|
def initialize(env, atts, md)
|
|
17
18
|
@env = env
|
|
@@ -20,6 +21,9 @@ module Syntropy
|
|
|
20
21
|
@module_loader = env[:module_loader]
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
# Returns the controller proc.
|
|
25
|
+
#
|
|
26
|
+
# @return [Proc] controller proc
|
|
23
27
|
def to_proc
|
|
24
28
|
->(req) {
|
|
25
29
|
case req.method
|
|
@@ -37,6 +41,10 @@ module Syntropy
|
|
|
37
41
|
|
|
38
42
|
private
|
|
39
43
|
|
|
44
|
+
# Process any Papercraft embeds in the markdown file by rendering them
|
|
45
|
+
# into the markdown code.
|
|
46
|
+
#
|
|
47
|
+
# @return [String] Markdown with rendered embeds
|
|
40
48
|
def process_md_embeds
|
|
41
49
|
return @md if @embedded_templates&.empty?
|
|
42
50
|
|
|
@@ -48,23 +56,40 @@ module Syntropy
|
|
|
48
56
|
}
|
|
49
57
|
end
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
# Prepares a snippet template by writing it to a temporary file and
|
|
60
|
+
# instance_eval'ing the code, returning a Papercraft template.
|
|
61
|
+
#
|
|
62
|
+
# @param snippet [String] Papercraft snippet
|
|
63
|
+
# @param _location [String] currently not used
|
|
64
|
+
# @return [Proc] Papercraft template
|
|
65
|
+
def prepare_snippet_template(snippet, _location = nil)
|
|
52
66
|
fn = "/tmp/snippet-#{SecureRandom.hex(8)}.rb"
|
|
53
67
|
src = "->() do\n#{snippet}\nend"
|
|
54
68
|
IO.write(fn, src)
|
|
55
69
|
instance_eval src, fn
|
|
56
70
|
end
|
|
57
71
|
|
|
72
|
+
# Renders the given markdown to HTML.
|
|
73
|
+
#
|
|
74
|
+
# @param md [String] markdown
|
|
75
|
+
# @return [String] HTML
|
|
58
76
|
def render(md)
|
|
59
77
|
@template ||= make_template
|
|
60
78
|
Papercraft.html(@template, md: md, **@atts)
|
|
61
79
|
end
|
|
62
80
|
|
|
81
|
+
# Makes a Markdown rendering template based on the corresponding layout.
|
|
82
|
+
#
|
|
83
|
+
# @return [Proc] Papercraft template
|
|
63
84
|
def make_template
|
|
64
85
|
layout = make_layout
|
|
65
86
|
Papercraft.apply(layout) { |md:, **| markdown(md) }
|
|
66
87
|
end
|
|
67
88
|
|
|
89
|
+
# Makes a layout template by loading the layout module. If no layout
|
|
90
|
+
# module was specified, returns the default layout.
|
|
91
|
+
#
|
|
92
|
+
# @return [Proc] Layout template
|
|
68
93
|
def make_layout
|
|
69
94
|
return default_layout if !@atts[:layout]
|
|
70
95
|
raise Error, 'Missing module loader' if !@module_loader
|
|
@@ -72,6 +97,9 @@ module Syntropy
|
|
|
72
97
|
@module_loader.load("_layout/#{@atts[:layout]}")
|
|
73
98
|
end
|
|
74
99
|
|
|
100
|
+
# Creates a default layout template.
|
|
101
|
+
#
|
|
102
|
+
# @return [Proc] Default layout template
|
|
75
103
|
def default_layout
|
|
76
104
|
->(**atts) {
|
|
77
105
|
html5 {
|
|
@@ -101,26 +129,26 @@ module Syntropy
|
|
|
101
129
|
parse_md(atts, md)
|
|
102
130
|
end
|
|
103
131
|
|
|
132
|
+
# Parses the markdown content, returning the attributes and cleaned up
|
|
133
|
+
# markdown.
|
|
134
|
+
#
|
|
135
|
+
# @param atts [Hash] attributes hash
|
|
136
|
+
# @param md [String] markdown file
|
|
137
|
+
# @return [Array<atts, md>] Array containing attributes and clean markdown
|
|
104
138
|
def parse_md(atts, md)
|
|
105
|
-
|
|
106
|
-
[atts,
|
|
139
|
+
md = parse_content(atts, md)
|
|
140
|
+
[atts, md]
|
|
107
141
|
end
|
|
108
142
|
|
|
143
|
+
# Creates a Markdown file contoroller for the given environment,
|
|
144
|
+
# attributes and markdown.
|
|
145
|
+
#
|
|
146
|
+
# @param env [Hash] app environment
|
|
147
|
+
# @param atts [Hash] markdown file attributes
|
|
148
|
+
# @param md [String] markdown
|
|
149
|
+
# @return [Markdown::Controller] controller instance
|
|
109
150
|
def make_controller(env, atts, md)
|
|
110
151
|
Controller.new(env, atts, md).to_proc
|
|
111
|
-
# layout = setup_layout_template(env, atts)
|
|
112
|
-
|
|
113
|
-
# ->(req) {
|
|
114
|
-
# case req.method
|
|
115
|
-
# when 'head'
|
|
116
|
-
# req.respond_html(nil)
|
|
117
|
-
# when 'get'
|
|
118
|
-
# html = render_md(env, atts, md)
|
|
119
|
-
# req.respond_html(html)
|
|
120
|
-
# else
|
|
121
|
-
# req.respond(nil, ':status' => HTTP::METHOD_NOT_ALLOWED)
|
|
122
|
-
# end
|
|
123
|
-
# }
|
|
124
152
|
end
|
|
125
153
|
|
|
126
154
|
private
|
|
@@ -165,10 +193,6 @@ module Syntropy
|
|
|
165
193
|
path.gsub(/#{app_root}/, '').gsub(/\.md$/, '')
|
|
166
194
|
end
|
|
167
195
|
end
|
|
168
|
-
|
|
169
|
-
def render_md(env, atts, md)
|
|
170
|
-
|
|
171
|
-
end
|
|
172
196
|
end
|
|
173
197
|
end
|
|
174
198
|
end
|
|
@@ -44,11 +44,11 @@ module Syntropy
|
|
|
44
44
|
#
|
|
45
45
|
# @param ref [String] module reference
|
|
46
46
|
# @return [any] export value
|
|
47
|
-
def load(ref,
|
|
47
|
+
def load(ref, raise_on_missing_export: true)
|
|
48
48
|
lock do
|
|
49
49
|
ref = "/#{ref}" if ref !~ /^\//
|
|
50
50
|
if !(entry = @modules[ref])
|
|
51
|
-
entry = load_module(ref,
|
|
51
|
+
entry = load_module(ref, raise_on_missing_export:)
|
|
52
52
|
return if !entry
|
|
53
53
|
|
|
54
54
|
@modules[ref] = entry
|
|
@@ -86,6 +86,8 @@ module Syntropy
|
|
|
86
86
|
private
|
|
87
87
|
|
|
88
88
|
# Synchronizes access to the module loader state
|
|
89
|
+
#
|
|
90
|
+
# @return [any] block return value
|
|
89
91
|
def lock
|
|
90
92
|
return yield if @lock_holder == Fiber.current
|
|
91
93
|
|
|
@@ -142,20 +144,25 @@ module Syntropy
|
|
|
142
144
|
#
|
|
143
145
|
# @param ref [String] module reference
|
|
144
146
|
# @return [Hash] module entry
|
|
145
|
-
def load_module(ref,
|
|
147
|
+
def load_module(ref, raise_on_missing_export: true)
|
|
146
148
|
ref = "/#{ref}" if ref !~ /^\//
|
|
147
149
|
fn = File.expand_path(File.join(@app_root, "#{ref}.rb"))
|
|
148
150
|
if !File.file?(fn)
|
|
149
|
-
raise Syntropy::Error, "File not found #{fn}" if
|
|
151
|
+
raise Syntropy::Error, "File not found #{fn}" if raise_on_missing_export
|
|
150
152
|
|
|
151
153
|
return
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
raise Syntropy::Error, "Circular dependency detected" if @loading.include?(ref)
|
|
155
|
-
do_load_module(ref, fn,
|
|
157
|
+
do_load_module(ref, fn, raise_on_missing_export:)
|
|
156
158
|
end
|
|
157
159
|
|
|
158
|
-
|
|
160
|
+
# Loads a module.
|
|
161
|
+
#
|
|
162
|
+
# @param ref [String] module reference
|
|
163
|
+
# @param fn [String] module filename
|
|
164
|
+
# @param raise_on_missing_export [bool] whether to raise on missing export
|
|
165
|
+
def do_load_module(ref, fn, raise_on_missing_export:)
|
|
159
166
|
@loading << ref
|
|
160
167
|
@fn_map[fn] = ref
|
|
161
168
|
code = read_file(fn)
|
|
@@ -163,7 +170,7 @@ module Syntropy
|
|
|
163
170
|
mod = Syntropy::ModuleContext.new(env, code, fn, @extensions)
|
|
164
171
|
add_dependencies(ref, mod.__dependencies__)
|
|
165
172
|
export_value = transform_module_export_value(
|
|
166
|
-
mod
|
|
173
|
+
mod, fn, raise_on_missing_export:
|
|
167
174
|
)
|
|
168
175
|
@env[:logger]&.info(message: "Loaded module at #{fn}")
|
|
169
176
|
|
|
@@ -180,6 +187,10 @@ module Syntropy
|
|
|
180
187
|
@loading.delete(ref)
|
|
181
188
|
end
|
|
182
189
|
|
|
190
|
+
# Returns a file's content.
|
|
191
|
+
#
|
|
192
|
+
# @param fn [String] filename
|
|
193
|
+
# @return [String] file content
|
|
183
194
|
def read_file(fn)
|
|
184
195
|
@machine.open(fn, UM::O_RDONLY) { |fd|
|
|
185
196
|
buf = +''
|
|
@@ -199,22 +210,24 @@ module Syntropy
|
|
|
199
210
|
(clean == '') ? '/' : clean
|
|
200
211
|
end
|
|
201
212
|
|
|
202
|
-
# Transforms the given export value. If the value is nil, an
|
|
203
|
-
# raised.
|
|
213
|
+
# Transforms the given module's export value. If the value is nil, an
|
|
214
|
+
# exception is raised.
|
|
204
215
|
#
|
|
205
|
-
# @param
|
|
216
|
+
# @param mod [Syntropy::ModuleContext] module context
|
|
206
217
|
# @return [any] transformed value
|
|
207
|
-
def transform_module_export_value(
|
|
208
|
-
case
|
|
218
|
+
def transform_module_export_value(mod, fn, raise_on_missing_export:)
|
|
219
|
+
case (value = mod.__export_value__)
|
|
209
220
|
when nil
|
|
210
|
-
|
|
221
|
+
mod
|
|
211
222
|
when String
|
|
212
|
-
->(req) { req.respond(
|
|
223
|
+
->(req) { req.respond(value) }
|
|
213
224
|
else
|
|
214
|
-
|
|
225
|
+
value
|
|
215
226
|
end
|
|
216
227
|
end
|
|
217
228
|
|
|
229
|
+
# Handles a syntax error encountered while loading a module by generating a
|
|
230
|
+
# new exception with a backtrace including the syntax error location.
|
|
218
231
|
def handle_syntax_error(fn, e)
|
|
219
232
|
$stderr.puts("\n#{e.message}") if !Syntropy.test_mode
|
|
220
233
|
m = e.message.match(/^(.+): syntax/)
|
|
@@ -249,7 +262,7 @@ module Syntropy
|
|
|
249
262
|
# @param env [Hash] environment hash
|
|
250
263
|
# @param code [String] module source code
|
|
251
264
|
# @param fn [String] module filename
|
|
252
|
-
# @param extensions [Array<Module>, Module]
|
|
265
|
+
# @param extensions [Array<Module>, Module]
|
|
253
266
|
# @return [void]
|
|
254
267
|
def initialize(env, code, fn, extensions)
|
|
255
268
|
@env = env
|
|
@@ -11,6 +11,10 @@ module Syntropy
|
|
|
11
11
|
module ResponseMethods
|
|
12
12
|
WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
13
13
|
|
|
14
|
+
# Upgrades a request to the WebSocket protocol.
|
|
15
|
+
#
|
|
16
|
+
# @param custom_headers [Hash, nil] additional response headers
|
|
17
|
+
# @return [void]
|
|
14
18
|
def upgrade_to_websocket(custom_headers = nil)
|
|
15
19
|
key = "#{headers['sec-websocket-key']}#{WEBSOCKET_GUID}"
|
|
16
20
|
upgrade_headers = {
|
|
@@ -22,21 +26,42 @@ module Syntropy
|
|
|
22
26
|
adapter.websocket_connection(self)
|
|
23
27
|
end
|
|
24
28
|
|
|
29
|
+
# Responds with a redirect to the given URL. If a relative path is provided
|
|
30
|
+
# as the URL, the absolute URL path will be calculated relative to the
|
|
31
|
+
# current request's path.
|
|
32
|
+
#
|
|
33
|
+
# @param url [String] redirect URL
|
|
34
|
+
# @param status [Integer] HTTP status code
|
|
35
|
+
# @return [void]
|
|
25
36
|
def redirect(url, status = HTTP::FOUND)
|
|
26
37
|
url = File.expand_path(File.join(path, url)) if url =~ /^\./
|
|
27
38
|
respond(nil, ':status' => status, 'Location' => url)
|
|
28
39
|
end
|
|
29
40
|
|
|
41
|
+
# Redirects to the same URL, except with HTTPS.
|
|
42
|
+
#
|
|
43
|
+
# @param status [Integer] HTTP status code
|
|
44
|
+
# @return [void]
|
|
30
45
|
def redirect_to_https(status = HTTP::MOVED_PERMANENTLY)
|
|
31
46
|
secure_uri = "https://#{host}#{uri}"
|
|
32
47
|
redirect(secure_uri, status)
|
|
33
48
|
end
|
|
34
49
|
|
|
50
|
+
# Redirect to the same path and query, on a different host.
|
|
51
|
+
#
|
|
52
|
+
# @param new_host [String] new host name
|
|
53
|
+
# @param status [Integer] HTTP status code
|
|
54
|
+
# @return [void]
|
|
35
55
|
def redirect_to_host(new_host, status = HTTP::FOUND)
|
|
36
56
|
secure_uri = "//#{new_host}#{uri}"
|
|
37
57
|
redirect(secure_uri, status)
|
|
38
58
|
end
|
|
39
59
|
|
|
60
|
+
# Serves a static file as a response, with cache headers.
|
|
61
|
+
#
|
|
62
|
+
# @param path [String] file path
|
|
63
|
+
# @param opts [Hash] options
|
|
64
|
+
# @return [void]
|
|
40
65
|
def serve_file(path, opts = {})
|
|
41
66
|
full_path = file_full_path(path, opts)
|
|
42
67
|
stat = File.stat(full_path)
|
|
@@ -59,6 +84,12 @@ module Syntropy
|
|
|
59
84
|
respond(nil, ':status' => HTTP::NOT_FOUND)
|
|
60
85
|
end
|
|
61
86
|
|
|
87
|
+
# Validates a static file cache against the request cache headers. Returns
|
|
88
|
+
# true if cache headers are valid.
|
|
89
|
+
#
|
|
90
|
+
# @param etag [String] ETag
|
|
91
|
+
# @param last_modified [String] last-modified value
|
|
92
|
+
# @return [bool] are cache headers valid
|
|
62
93
|
def validate_static_file_cache(etag, last_modified)
|
|
63
94
|
if (none_match = headers['if-none-match'])
|
|
64
95
|
return true if none_match == etag
|
|
@@ -70,6 +101,11 @@ module Syntropy
|
|
|
70
101
|
false
|
|
71
102
|
end
|
|
72
103
|
|
|
104
|
+
# Returns a fully qualified file path based on the given options.
|
|
105
|
+
#
|
|
106
|
+
# @param path [String] file path
|
|
107
|
+
# @param opts [Hash] options hash
|
|
108
|
+
# @return [String] qualified file path
|
|
73
109
|
def file_full_path(path, opts)
|
|
74
110
|
if (base_path = opts[:base_path])
|
|
75
111
|
File.join(opts[:base_path], path)
|
|
@@ -78,10 +114,13 @@ module Syntropy
|
|
|
78
114
|
end
|
|
79
115
|
end
|
|
80
116
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
# Renders a static file response.
|
|
118
|
+
#
|
|
119
|
+
# @param path [String] file path
|
|
120
|
+
# @param etag [String] ETag
|
|
121
|
+
# @param last_modified [String] last-modified value
|
|
122
|
+
# @param opts [Hash] options
|
|
123
|
+
# @return [void]
|
|
85
124
|
def respond_with_static_file(path, etag, last_modified, opts)
|
|
86
125
|
cache_headers = (etag || last_modified) ? {
|
|
87
126
|
'etag' => etag,
|
|
@@ -91,14 +130,29 @@ module Syntropy
|
|
|
91
130
|
adapter.respond_with_static_file(self, path, opts, cache_headers)
|
|
92
131
|
end
|
|
93
132
|
|
|
133
|
+
# Set additional response headers.
|
|
134
|
+
#
|
|
135
|
+
# @param headers [Hash] response headers
|
|
136
|
+
# @return [void]
|
|
94
137
|
def set_response_headers(headers)
|
|
95
138
|
adapter.set_response_headers(headers)
|
|
96
139
|
end
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
# Sets a response cookie.
|
|
142
|
+
#
|
|
143
|
+
# @param key [String] cookie name
|
|
144
|
+
# @param value [String] cookie value
|
|
145
|
+
# @return [void]
|
|
146
|
+
def set_cookie(key, value)
|
|
147
|
+
adapter.set_cookie(key, value)
|
|
100
148
|
end
|
|
101
149
|
|
|
150
|
+
# Upgrades the connection to the given protocol, yielding the connection to
|
|
151
|
+
# the given block.
|
|
152
|
+
#
|
|
153
|
+
# @param protocol [String] upgraded protocol
|
|
154
|
+
# @param custom_headers [Hash, nil] additional response headers
|
|
155
|
+
# @return [void]
|
|
102
156
|
def upgrade(protocol, custom_headers = nil, &block)
|
|
103
157
|
upgrade_headers = {
|
|
104
158
|
':status' => HTTP::SWITCHING_PROTOCOLS,
|
|
@@ -169,6 +223,11 @@ module Syntropy
|
|
|
169
223
|
end
|
|
170
224
|
end
|
|
171
225
|
|
|
226
|
+
# Renders an HTML repsonse with HTML content type.
|
|
227
|
+
#
|
|
228
|
+
# @param html [String] HTML response body
|
|
229
|
+
# @param headers [Hash] additional response headers
|
|
230
|
+
# @return [void]
|
|
172
231
|
def respond_html(html, **headers)
|
|
173
232
|
respond(
|
|
174
233
|
html,
|
|
@@ -177,6 +236,11 @@ module Syntropy
|
|
|
177
236
|
)
|
|
178
237
|
end
|
|
179
238
|
|
|
239
|
+
# Renders a JSON repsonse with JSON content type.
|
|
240
|
+
#
|
|
241
|
+
# @param obj [any] obj to render into JSON
|
|
242
|
+
# @param headers [Hash] additional response headers
|
|
243
|
+
# @return [void]
|
|
180
244
|
def respond_json(obj, **headers)
|
|
181
245
|
respond(
|
|
182
246
|
JSON.dump(obj),
|
|
@@ -185,6 +249,11 @@ module Syntropy
|
|
|
185
249
|
)
|
|
186
250
|
end
|
|
187
251
|
|
|
252
|
+
# Renders a nicely-formatted JSON repsonse with JSON content type.
|
|
253
|
+
#
|
|
254
|
+
# @param obj [any] obj to render into JSON
|
|
255
|
+
# @param headers [Hash] additional response headers
|
|
256
|
+
# @return [void]
|
|
188
257
|
def json_pretty_response(obj, **headers)
|
|
189
258
|
respond(
|
|
190
259
|
JSON.pretty_generate(obj),
|
|
@@ -18,6 +18,12 @@ module Syntropy
|
|
|
18
18
|
raise Syntropy::Error.method_not_allowed
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Validates the request's content-type against the given accepted content
|
|
22
|
+
# types. If the content type does not match the given accepted list, raises
|
|
23
|
+
# an error.
|
|
24
|
+
#
|
|
25
|
+
# @param accepted [Array<String>] array of accepted content types
|
|
26
|
+
# @return [String] accepted content type
|
|
21
27
|
def validate_content_type(*accepted)
|
|
22
28
|
ct = content_type
|
|
23
29
|
return ct if accepted.include?(ct)
|
|
@@ -8,6 +8,12 @@ module Syntropy
|
|
|
8
8
|
class ConnectionPool
|
|
9
9
|
attr_reader :count
|
|
10
10
|
|
|
11
|
+
# Initializes the connection pool.
|
|
12
|
+
#
|
|
13
|
+
# @param machine [UringMachine] machine instance
|
|
14
|
+
# @param fn [String] database filename
|
|
15
|
+
# @param max_conn [Integer] maximum number of connections
|
|
16
|
+
# @return [void]
|
|
11
17
|
def initialize(machine, fn, max_conn)
|
|
12
18
|
@machine = machine
|
|
13
19
|
@fn = fn
|
|
@@ -17,6 +23,11 @@ module Syntropy
|
|
|
17
23
|
@key = :"connection_pool_#{object_id}"
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
# Checks out a database connection, passing it to the given block. This
|
|
27
|
+
# method is reentrant - called from the same fiber, it will yield the same
|
|
28
|
+
# database connection.
|
|
29
|
+
#
|
|
30
|
+
# @return [Extralite::Database] database connection
|
|
20
31
|
def with_db
|
|
21
32
|
if (db = Thread.current[@key])
|
|
22
33
|
@machine.snooze
|
|
@@ -33,14 +44,27 @@ module Syntropy
|
|
|
33
44
|
end
|
|
34
45
|
end
|
|
35
46
|
|
|
47
|
+
# Performs the given query by acquiring a database connection and running
|
|
48
|
+
# the query.
|
|
49
|
+
#
|
|
50
|
+
# @param sql [String] SQL query
|
|
51
|
+
# @return [Array<Hash>] result rows
|
|
36
52
|
def query(sql, *, **, &)
|
|
37
53
|
with_db { it.query(sql, *, **, &) }
|
|
38
54
|
end
|
|
39
55
|
|
|
56
|
+
# Executes the given query by acquiring a database connection and running
|
|
57
|
+
# the query.
|
|
58
|
+
#
|
|
59
|
+
# @param sql [String] SQL query
|
|
60
|
+
# @return [Integer] number of changed rows
|
|
40
61
|
def execute(sql, *, **)
|
|
41
62
|
with_db { it.execute(sql, *, **) }
|
|
42
63
|
end
|
|
43
64
|
|
|
65
|
+
# Closes the connection pool by removing all connections.
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
44
68
|
def close
|
|
45
69
|
while @queue.count > 0
|
|
46
70
|
db = @machine.shift(@queue)
|
|
@@ -51,16 +75,26 @@ module Syntropy
|
|
|
51
75
|
|
|
52
76
|
private
|
|
53
77
|
|
|
78
|
+
# Checks out a connection from the pool.
|
|
79
|
+
#
|
|
80
|
+
# @return [Extralite::Database] database connection
|
|
54
81
|
def checkout
|
|
55
82
|
return make_db_instance if @queue.count == 0 && @count < @max_conn
|
|
56
83
|
|
|
57
84
|
@machine.shift(@queue)
|
|
58
85
|
end
|
|
59
86
|
|
|
87
|
+
# Checks in a connection to the pool.
|
|
88
|
+
#
|
|
89
|
+
# @param db [Extralite::Database] database connection
|
|
90
|
+
# @return [void]
|
|
60
91
|
def checkin(db)
|
|
61
92
|
@machine.push(@queue, db)
|
|
62
93
|
end
|
|
63
94
|
|
|
95
|
+
# Creates a database connection.
|
|
96
|
+
#
|
|
97
|
+
# @return [Extralite::Database] database connection
|
|
64
98
|
def make_db_instance
|
|
65
99
|
Extralite::Database.new(@fn, wal: true).tap do
|
|
66
100
|
@count += 1
|
data/lib/syntropy/test.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Syntropy
|
|
|
19
19
|
def global_env
|
|
20
20
|
@@global_env
|
|
21
21
|
end
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
# Gets/sets app environment for tests
|
|
24
24
|
attr_accessor :env
|
|
25
25
|
end
|
|
@@ -38,8 +38,8 @@ module Syntropy
|
|
|
38
38
|
#
|
|
39
39
|
# @param ref [String] module reference
|
|
40
40
|
# @return [any] module
|
|
41
|
-
def load_module(ref,
|
|
42
|
-
app.module_loader.load(ref,
|
|
41
|
+
def load_module(ref, raise_on_missing_export: true)
|
|
42
|
+
app.module_loader.load(ref, raise_on_missing_export:)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Makes an HTTP request to the test app.
|
|
@@ -145,7 +145,7 @@ module Syntropy
|
|
|
145
145
|
)
|
|
146
146
|
@test_harness = Syntropy::TestHarness.new(@app)
|
|
147
147
|
|
|
148
|
-
@db = load_module('/_lib/storage',
|
|
148
|
+
@db = load_module('/_lib/storage', raise_on_missing_export: false)
|
|
149
149
|
@db&.migrate! if @db.respond_to?(:migrate!)
|
|
150
150
|
end
|
|
151
151
|
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
|
@@ -72,6 +72,12 @@ module Syntropy
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Loads the app config module based on the environment's config root and
|
|
76
|
+
# mode: (test, dev, production). The module's exported value is assigned to
|
|
77
|
+
# env[:config].
|
|
78
|
+
#
|
|
79
|
+
# @param env [Hash] app environment
|
|
80
|
+
# @return [void]
|
|
75
81
|
def load_config(env)
|
|
76
82
|
return if !env[:config_root]
|
|
77
83
|
|
|
@@ -81,7 +87,7 @@ module Syntropy
|
|
|
81
87
|
machine: UM.new(size: 8)
|
|
82
88
|
)
|
|
83
89
|
loader = ModuleLoader.new(loader_env)
|
|
84
|
-
if (config = loader.load(env[:mode],
|
|
90
|
+
if (config = loader.load(env[:mode], raise_on_missing_export: false))
|
|
85
91
|
env[:config] = config
|
|
86
92
|
end
|
|
87
93
|
end
|
data/syntropy.gemspec
CHANGED
|
@@ -21,9 +21,9 @@ Gem::Specification.new do |s|
|
|
|
21
21
|
s.required_ruby_version = '>= 3.4'
|
|
22
22
|
s.executables = ['syntropy']
|
|
23
23
|
|
|
24
|
-
s.add_dependency 'extralite', '~>
|
|
25
|
-
s.add_dependency 'papercraft', '~>3.2.
|
|
26
|
-
s.add_dependency 'uringmachine', '~>1.0.
|
|
24
|
+
s.add_dependency 'extralite', '~>3.0.1'
|
|
25
|
+
s.add_dependency 'papercraft', '~>3.2.2'
|
|
26
|
+
s.add_dependency 'uringmachine', '~>1.0.4'
|
|
27
27
|
|
|
28
28
|
s.add_dependency 'json'
|
|
29
29
|
s.add_dependency 'base64'
|
|
@@ -31,6 +31,6 @@ Gem::Specification.new do |s|
|
|
|
31
31
|
s.add_dependency 'irb'
|
|
32
32
|
s.add_dependency 'minitest'
|
|
33
33
|
|
|
34
|
-
s.add_development_dependency 'rake'
|
|
34
|
+
s.add_development_dependency 'rake'
|
|
35
35
|
s.add_development_dependency 'solargraph'
|
|
36
36
|
end
|
data/test/test_markdown.rb
CHANGED
|
@@ -60,7 +60,7 @@ class MarkdownControllerTest < Minitest::Test
|
|
|
60
60
|
@controller = nil
|
|
61
61
|
@test_harness = Syntropy::TestHarness.new(->(req) { @controller.(req) })
|
|
62
62
|
end
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
def test_markdown_renderer_no_layout
|
|
65
65
|
md = <<~MD
|
|
66
66
|
foo *bar*
|
|
@@ -230,8 +230,8 @@ class MarkdownControllerTest < Minitest::Test
|
|
|
230
230
|
md = <<~MD
|
|
231
231
|
```ruby
|
|
232
232
|
# render: true
|
|
233
|
-
@
|
|
234
|
-
h3 (@
|
|
233
|
+
@count ||= 0
|
|
234
|
+
h3 (@count += 1)
|
|
235
235
|
```
|
|
236
236
|
bar
|
|
237
237
|
MD
|
data/test/test_module_loader.rb
CHANGED
|
@@ -16,7 +16,10 @@ class ModuleLoaderTest < Minitest::Test
|
|
|
16
16
|
assert_equal 42, mod.bar
|
|
17
17
|
|
|
18
18
|
assert_raises(Syntropy::Error) { @loader.load('_lib/missing-module') }
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
mod = @loader.load('_lib/missing-export')
|
|
21
|
+
assert_kind_of Syntropy::ModuleContext, mod
|
|
22
|
+
assert_equal :bar, mod.call
|
|
20
23
|
|
|
21
24
|
mod = @loader.load('_lib/callable')
|
|
22
25
|
assert_kind_of Syntropy::ModuleContext, mod
|