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
|
@@ -5,7 +5,7 @@ module Syntropy
|
|
|
5
5
|
# static files, markdown files, ruby modules, parametric routes, subtree routes,
|
|
6
6
|
# nested middleware and error handlers.
|
|
7
7
|
#
|
|
8
|
-
# A RoutingTree instance takes the given directory (
|
|
8
|
+
# A RoutingTree instance takes the given directory (app_root) and constructs a
|
|
9
9
|
# tree of route entries corresponding to the directory's contents. Finally, it
|
|
10
10
|
# generates an optimized router proc, which is used by the application to return
|
|
11
11
|
# a route entry for each incoming HTTP request.
|
|
@@ -41,15 +41,15 @@ module Syntropy
|
|
|
41
41
|
# allows you to prevent access through the HTTP server to protected or
|
|
42
42
|
# internal modules or files.
|
|
43
43
|
class RoutingTree
|
|
44
|
-
attr_reader :
|
|
44
|
+
attr_reader :app_root, :mount_path, :static_map, :dynamic_map, :root
|
|
45
45
|
|
|
46
46
|
# Initializes a new RoutingTree instance and computes the routing tree
|
|
47
47
|
#
|
|
48
|
-
# @param
|
|
48
|
+
# @param app_root [String] root directory of file tree
|
|
49
49
|
# @param mount_path [String] base URL path
|
|
50
50
|
# @return [void]
|
|
51
|
-
def initialize(
|
|
52
|
-
@
|
|
51
|
+
def initialize(app_root:, mount_path:, **env)
|
|
52
|
+
@app_root = app_root
|
|
53
53
|
@mount_path = mount_path
|
|
54
54
|
@static_map = {}
|
|
55
55
|
@dynamic_map = {}
|
|
@@ -72,7 +72,7 @@ module Syntropy
|
|
|
72
72
|
# @param fn [String] file path
|
|
73
73
|
# @return [String] clean path
|
|
74
74
|
def compute_clean_url_path(fn)
|
|
75
|
-
rel_path = fn.sub(@
|
|
75
|
+
rel_path = fn.sub(@app_root, '')
|
|
76
76
|
case rel_path
|
|
77
77
|
when /^(.*)\/index\.(md|rb|html)$/
|
|
78
78
|
Regexp.last_match(1).then { it == '' ? '/' : it }
|
|
@@ -88,7 +88,7 @@ module Syntropy
|
|
|
88
88
|
# @param fn [String] filename
|
|
89
89
|
# @return [String] relative path
|
|
90
90
|
def fn_to_rel_path(fn)
|
|
91
|
-
fn.sub(/^#{Regexp.escape(@
|
|
91
|
+
fn.sub(/^#{Regexp.escape(@app_root)}\//, '').sub(/\.[^\.]+$/, '')
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# Mounts the given applet on the routng tree at the given (absolute) mount
|
|
@@ -142,7 +142,7 @@ module Syntropy
|
|
|
142
142
|
#
|
|
143
143
|
# @return [Hash] root entry
|
|
144
144
|
def compute_tree
|
|
145
|
-
compute_route_directory(dir: @
|
|
145
|
+
compute_route_directory(dir: @app_root, rel_path: '/', parent: nil)
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
# Converts the given absolute path to a relative one (relative to the
|
|
@@ -233,7 +233,7 @@ module Syntropy
|
|
|
233
233
|
# @return [String, nil] file path if found
|
|
234
234
|
def find_aux_module_entry(dir, name)
|
|
235
235
|
fn = File.join(dir, name)
|
|
236
|
-
File.file?(fn) ?
|
|
236
|
+
File.file?(fn) ? { kind: :module, fn: } : nil
|
|
237
237
|
end
|
|
238
238
|
|
|
239
239
|
# Returns a hash mapping file/dir names to route entries.
|
|
@@ -247,10 +247,10 @@ module Syntropy
|
|
|
247
247
|
|
|
248
248
|
rel_path = compute_clean_url_path(fn)
|
|
249
249
|
child = if File.file?(fn)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
compute_route_file(fn:, rel_path:, parent:)
|
|
251
|
+
elsif File.directory?(fn)
|
|
252
|
+
compute_route_directory(dir: fn, rel_path:, parent:)
|
|
253
|
+
end
|
|
254
254
|
map[child_key(child)] = child if child
|
|
255
255
|
}
|
|
256
256
|
end
|
|
@@ -317,7 +317,6 @@ module Syntropy
|
|
|
317
317
|
set_index_route_target(parent:, path:, kind:, fn:, handle_subtree:)
|
|
318
318
|
end
|
|
319
319
|
|
|
320
|
-
|
|
321
320
|
# Sets an index route target for the given parent entry. Index files are
|
|
322
321
|
# applied as targets to the immediate containing directory. HTML index files
|
|
323
322
|
# are considered static and therefore not added to the routing tree.
|
|
@@ -329,7 +328,7 @@ module Syntropy
|
|
|
329
328
|
# @param handle_subtree [bool] whether the target handles the subtree
|
|
330
329
|
# @return [nil] (prevents addition of an index route)
|
|
331
330
|
def set_index_route_target(parent:, path:, kind:, fn:, handle_subtree: nil)
|
|
332
|
-
if
|
|
331
|
+
if parametric_route?(parent) || handle_subtree
|
|
333
332
|
@dynamic_map[path] = parent
|
|
334
333
|
parent[:target] = { kind:, fn: }
|
|
335
334
|
parent[:handle_subtree] = handle_subtree
|
|
@@ -383,7 +382,7 @@ module Syntropy
|
|
|
383
382
|
# @param entry [Hash] route entry
|
|
384
383
|
def make_route_entry(entry)
|
|
385
384
|
path = entry[:path]
|
|
386
|
-
if
|
|
385
|
+
if parametric_route?(entry) || entry[:handle_subtree]
|
|
387
386
|
@dynamic_map[path] = entry
|
|
388
387
|
else
|
|
389
388
|
entry[:static] = true
|
|
@@ -394,8 +393,8 @@ module Syntropy
|
|
|
394
393
|
# returns true if the route or any of its ancestors are parametric.
|
|
395
394
|
#
|
|
396
395
|
# @param entry [Hash] route entry
|
|
397
|
-
def
|
|
398
|
-
entry[:param] || (entry[:parent] &&
|
|
396
|
+
def parametric_route?(entry)
|
|
397
|
+
entry[:param] || (entry[:parent] && parametric_route?(entry[:parent]))
|
|
399
398
|
end
|
|
400
399
|
|
|
401
400
|
# Converts a relative URL path to absolute URL path.
|
|
@@ -468,7 +467,7 @@ module Syntropy
|
|
|
468
467
|
emit_router_proc_postlude(buffer, default_route_path: wildcard_root && @root[:path])
|
|
469
468
|
end
|
|
470
469
|
|
|
471
|
-
buffer
|
|
470
|
+
buffer # .tap { puts '*' * 40; puts it; puts }
|
|
472
471
|
end
|
|
473
472
|
|
|
474
473
|
# Emits optimized code for a childless wildcard router.
|
|
@@ -540,7 +539,7 @@ module Syntropy
|
|
|
540
539
|
return
|
|
541
540
|
end
|
|
542
541
|
|
|
543
|
-
if
|
|
542
|
+
if void_route?(entry)
|
|
544
543
|
parent = entry[:parent]
|
|
545
544
|
parametric_sibling = parent && parent[:children] && parent[:children]['[]']
|
|
546
545
|
if parametric_sibling
|
|
@@ -580,7 +579,7 @@ module Syntropy
|
|
|
580
579
|
# @param entry [Hash] route entry
|
|
581
580
|
# @return [Hash, nil] route target if exists
|
|
582
581
|
def find_target_in_subtree(entry)
|
|
583
|
-
entry[:children]&.
|
|
582
|
+
entry[:children]&.each_value { |e|
|
|
584
583
|
target = e[:target] || find_target_in_subtree(e)
|
|
585
584
|
return target if target
|
|
586
585
|
}
|
|
@@ -593,14 +592,14 @@ module Syntropy
|
|
|
593
592
|
#
|
|
594
593
|
# @param entry [Hash] route entry
|
|
595
594
|
# @return [bool]
|
|
596
|
-
def
|
|
595
|
+
def void_route?(entry)
|
|
597
596
|
return false if entry[:param] || entry[:target]
|
|
597
|
+
return true if entry[:static]
|
|
598
598
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
end
|
|
599
|
+
children = entry[:children]
|
|
600
|
+
return false if !children
|
|
601
|
+
|
|
602
|
+
return true if !children['[]'] && children.values.all? { void_route?(it) }
|
|
604
603
|
|
|
605
604
|
false
|
|
606
605
|
end
|
|
@@ -640,7 +639,7 @@ module Syntropy
|
|
|
640
639
|
|
|
641
640
|
elsif has_children
|
|
642
641
|
# otherwise look at the next segment
|
|
643
|
-
next if
|
|
642
|
+
next if void_route?(child_entry) && !param_entry
|
|
644
643
|
|
|
645
644
|
when_buffer = +''
|
|
646
645
|
visit_routing_tree_entry(buffer: when_buffer, entry: child_entry, indent: indent + 1, segment_idx: segment_idx + 1)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module Syntropy
|
|
8
|
+
# A Session object serves as storage for data associated with the user's
|
|
9
|
+
# browser session, such as flash data (for communicating notices and alerts).
|
|
10
|
+
# The session is modeled as a key-value store, where keys are strings, and
|
|
11
|
+
# values can be any value that can be represented in JSON, including arrays
|
|
12
|
+
# and hashes.
|
|
13
|
+
#
|
|
14
|
+
# The session data is stored as an HTTP cookie.
|
|
15
|
+
class Session
|
|
16
|
+
# Initializes the session.
|
|
17
|
+
#
|
|
18
|
+
# @param request [Syntropy::Request] associated request
|
|
19
|
+
# @return [void]
|
|
20
|
+
def initialize(request)
|
|
21
|
+
@request = request
|
|
22
|
+
@data = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns the value associated with the given key.
|
|
26
|
+
#
|
|
27
|
+
# @param key [String]
|
|
28
|
+
# @return [any] value
|
|
29
|
+
def [](key)
|
|
30
|
+
@data ||= load
|
|
31
|
+
@data[key]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Sets the value for the given key and updates the response session cookie.
|
|
35
|
+
#
|
|
36
|
+
# @param key [String]
|
|
37
|
+
# @param value [any]
|
|
38
|
+
# @return [void]
|
|
39
|
+
def []=(key, value)
|
|
40
|
+
@data ||= load
|
|
41
|
+
@data[key] = value
|
|
42
|
+
save(@data)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Deletes the given key-value pair and updates the response session cookie.
|
|
46
|
+
#
|
|
47
|
+
# @param key [String]
|
|
48
|
+
# @return [any] deleted value
|
|
49
|
+
def delete(key)
|
|
50
|
+
@data ||= load
|
|
51
|
+
value = @data.delete(key)
|
|
52
|
+
save(@data.empty? ? nil : @data)
|
|
53
|
+
value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Discards the session data, updating the response session cookie by
|
|
57
|
+
# emptying it.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
def discard
|
|
61
|
+
save(nil)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns the flash storage for the session.
|
|
65
|
+
#
|
|
66
|
+
# @return [Syntropy::Session::Flash]
|
|
67
|
+
def flash
|
|
68
|
+
@data ||= load
|
|
69
|
+
@flash ||= Flash.new(self)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Loads session data from the request session cookie.
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash] session data
|
|
77
|
+
def load
|
|
78
|
+
data = @request.cookies['__syntropy_session__']
|
|
79
|
+
return {} if !data
|
|
80
|
+
|
|
81
|
+
JSON.parse(Base64.decode64(data))
|
|
82
|
+
rescue JSON::ParserError
|
|
83
|
+
{}
|
|
84
|
+
ensure
|
|
85
|
+
@loaded = true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Saves session data to the response session cookie.
|
|
89
|
+
#
|
|
90
|
+
# @param data [Hash] session data
|
|
91
|
+
# @return [void]
|
|
92
|
+
def save(data)
|
|
93
|
+
cookie = data ? "#{Base64.strict_encode64(JSON.dump(data))}; Path=/; HttpOnly" : nil
|
|
94
|
+
@request.set_cookie('__syntropy_session__', cookie)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# NowFlash holds flash data for the current request.
|
|
99
|
+
class NowFlash
|
|
100
|
+
def initialize
|
|
101
|
+
@data = {}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns the value for the given key.
|
|
105
|
+
#
|
|
106
|
+
# @param key [Symbol]
|
|
107
|
+
# @return [any] flash value
|
|
108
|
+
def [](key)
|
|
109
|
+
@data[key.to_s]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Sets the value for the given key.
|
|
113
|
+
#
|
|
114
|
+
# @param key [Symbol]
|
|
115
|
+
# @param value [any]
|
|
116
|
+
# @return [any] value
|
|
117
|
+
def []=(key, value)
|
|
118
|
+
@data[key.to_s] = value
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Iterates through the flash storage, yielding each key-value pair to the
|
|
122
|
+
# given block.
|
|
123
|
+
#
|
|
124
|
+
# @return [void]
|
|
125
|
+
def each(&block)
|
|
126
|
+
@data.each { |k, v| block.(k.to_sym, v) }
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Flash acts as a special storage mechanism for transient information that
|
|
131
|
+
# can be passsed between consecutive requests in the same session. Flash
|
|
132
|
+
# values can be set in order to be retrieved in the next response. Reading
|
|
133
|
+
# from flash storage will return data that was set in the previous request.
|
|
134
|
+
# Data written to the flash storage will only be available to the next
|
|
135
|
+
# request. You can also set flash data that will be available to the current
|
|
136
|
+
# request by using Flash#now.
|
|
137
|
+
#
|
|
138
|
+
# In order to keep the read flash data (set in the previous request) and
|
|
139
|
+
# make it available to the next request, use Flash#keep.
|
|
140
|
+
class Flash
|
|
141
|
+
# Initializes the flash storage.
|
|
142
|
+
#
|
|
143
|
+
# @return [void]
|
|
144
|
+
def initialize(session)
|
|
145
|
+
@session = session
|
|
146
|
+
@current_flash_data = @session['_flash']
|
|
147
|
+
@session.delete('_flash') if @current_flash_data
|
|
148
|
+
@current_flash_data ||= {}
|
|
149
|
+
@future_flash_data = {}
|
|
150
|
+
@now_flash_data = NowFlash.new
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Reads data from flash storage for the given key. The value would have
|
|
154
|
+
# been set in the previous request.
|
|
155
|
+
#
|
|
156
|
+
# @param key [Symbol]
|
|
157
|
+
# @return [any] value
|
|
158
|
+
def [](key)
|
|
159
|
+
key = key.to_s
|
|
160
|
+
@now_flash_data[key] || @current_flash_data[key]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Sets the flash storage value for the given key. The value would be
|
|
164
|
+
# available to the next request.
|
|
165
|
+
#
|
|
166
|
+
# @param key [Symbol]
|
|
167
|
+
# @param value [any]
|
|
168
|
+
# @return [void]
|
|
169
|
+
def []=(key, value)
|
|
170
|
+
key = key.to_s
|
|
171
|
+
@future_flash_data[key] = value
|
|
172
|
+
@session['_flash'] = @future_flash_data
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Iterates through the flash storage, passing each key-value pair to the
|
|
176
|
+
# given block.
|
|
177
|
+
#
|
|
178
|
+
# @return [void]
|
|
179
|
+
def each(&block)
|
|
180
|
+
@current_flash_data.each_pair { |k, v| block.(k.to_sym, v) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Persists any flash data set in the previous request to the next request.
|
|
184
|
+
#
|
|
185
|
+
# @return [void]
|
|
186
|
+
def keep
|
|
187
|
+
@future_flash_data = @current_flash_data.merge!(@future_flash_data)
|
|
188
|
+
@session['_flash'] = @future_flash_data
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Returns the flash storage for the current request.
|
|
192
|
+
#
|
|
193
|
+
# @return [Syntropy::NowFlash]
|
|
194
|
+
def now
|
|
195
|
+
@now_flash_data
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
data/lib/syntropy/side_run.rb
CHANGED
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
require 'etc'
|
|
4
4
|
|
|
5
5
|
module Syntropy
|
|
6
|
+
# SideRun implements running an operation on a separate thread.
|
|
6
7
|
module SideRun
|
|
7
8
|
class << self
|
|
9
|
+
# Runs the given block on a separate thread, using UringMachine to wait
|
|
10
|
+
# for the operation to complete. If the operation results in a raised
|
|
11
|
+
# exception, that exception will be reraised in the context of the waiting
|
|
12
|
+
# fiber.
|
|
13
|
+
#
|
|
14
|
+
# @param machine [UringMachine] machine instance
|
|
15
|
+
# @return [any] operation return value
|
|
8
16
|
def call(machine, &block)
|
|
9
17
|
setup if !@queue
|
|
10
18
|
|
|
@@ -15,6 +23,11 @@ module Syntropy
|
|
|
15
23
|
result.is_a?(Exception) ? (raise result) : result
|
|
16
24
|
end
|
|
17
25
|
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Sets up a thread pool for side-running operations.
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
18
31
|
def setup
|
|
19
32
|
@queue = UM::Queue.new
|
|
20
33
|
count = (Etc.nprocessors - 1).clamp(2..6)
|
|
@@ -23,9 +36,13 @@ module Syntropy
|
|
|
23
36
|
}
|
|
24
37
|
end
|
|
25
38
|
|
|
39
|
+
# Runs worker loop for running side-run operations.
|
|
40
|
+
#
|
|
41
|
+
# @param queue [UringMachine::Queue] queue for pulling operations
|
|
42
|
+
# @return [void]
|
|
26
43
|
def side_run_worker(queue)
|
|
27
44
|
machine = UM.new
|
|
28
|
-
loop {
|
|
45
|
+
loop { run_op(machine, queue) }
|
|
29
46
|
rescue UM::Terminate
|
|
30
47
|
# # We can also add a timeout here
|
|
31
48
|
# t0 = Time.now
|
|
@@ -34,7 +51,13 @@ module Syntropy
|
|
|
34
51
|
# end
|
|
35
52
|
end
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
# Pulls an operation from the given queue and runs it, pushing its return
|
|
55
|
+
# value to the corresponding mailbox.
|
|
56
|
+
#
|
|
57
|
+
# @param machine [UringMachine] machine instance
|
|
58
|
+
# @param queue [UringMachine::Queue] op queue
|
|
59
|
+
# @return [void]
|
|
60
|
+
def run_op(machine, queue)
|
|
38
61
|
response_mailbox, closure = machine.shift(queue)
|
|
39
62
|
result = closure.call
|
|
40
63
|
machine.push(response_mailbox, result)
|
data/lib/syntropy/test.rb
CHANGED
|
@@ -7,27 +7,49 @@ require 'json'
|
|
|
7
7
|
require 'uri'
|
|
8
8
|
|
|
9
9
|
module Syntropy
|
|
10
|
+
# Test provides a class for testing a Syntropy app, based on Minitest.
|
|
10
11
|
class Test < Minitest::Test
|
|
11
12
|
HTTP = Syntropy::HTTP
|
|
12
13
|
|
|
14
|
+
# Sets the app environment for all Syntropy tests.
|
|
15
|
+
#
|
|
16
|
+
# @param env [Hash] app environment hash
|
|
17
|
+
# @return [void]
|
|
13
18
|
def self.env=(env)
|
|
14
19
|
@@env = env
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
attr_reader :machine, :app
|
|
18
23
|
|
|
24
|
+
# Returns the test environment.
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash] test app environment
|
|
19
27
|
def env
|
|
20
28
|
@@env
|
|
21
29
|
end
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
# Loads and returns a module with the given reference.
|
|
32
|
+
#
|
|
33
|
+
# @param ref [String] module reference
|
|
34
|
+
# @return [any] module
|
|
35
|
+
def load_module(ref, raise_on_missing: true)
|
|
36
|
+
app.module_loader.load(ref, raise_on_missing:)
|
|
25
37
|
end
|
|
26
38
|
|
|
39
|
+
# Makes an HTTP request to the test app.
|
|
40
|
+
#
|
|
41
|
+
# @param headers [Hash] request headers
|
|
42
|
+
# @param body [String, nil] request body
|
|
43
|
+
# @return [Syntropy::Request]
|
|
27
44
|
def http_request(headers, body = nil)
|
|
28
45
|
@test_harness.request(headers, body)
|
|
29
46
|
end
|
|
30
47
|
|
|
48
|
+
# Makes an HTTP GET request to the test app.
|
|
49
|
+
#
|
|
50
|
+
# @param path [String] request path
|
|
51
|
+
# @param headers [Hash] request headers
|
|
52
|
+
# @return [Syntropy::Request]
|
|
31
53
|
def get(path, **headers)
|
|
32
54
|
http_request(
|
|
33
55
|
headers.merge(
|
|
@@ -37,6 +59,13 @@ module Syntropy
|
|
|
37
59
|
)
|
|
38
60
|
end
|
|
39
61
|
|
|
62
|
+
# Makes an HTTP POST request to the test app.
|
|
63
|
+
#
|
|
64
|
+
# @param path [String] request path
|
|
65
|
+
# @param content_type [String, nil] content MIME type
|
|
66
|
+
# @param body [String] request body
|
|
67
|
+
# @param headers [Hash] request headers
|
|
68
|
+
# @return [Syntropy::Request]
|
|
40
69
|
def post(path, content_type, body, **headers)
|
|
41
70
|
headers = headers.merge('content-type' => content_type) if content_type
|
|
42
71
|
http_request(
|
|
@@ -50,26 +79,52 @@ module Syntropy
|
|
|
50
79
|
)
|
|
51
80
|
end
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
# Makes an HTTP POST request to the test app with a "application/json"
|
|
83
|
+
# content type. The given object is converted to JSON and sent as the
|
|
84
|
+
# request body.
|
|
85
|
+
#
|
|
86
|
+
# @param path [String] request path
|
|
87
|
+
# @param data [any] data
|
|
88
|
+
# @return [Syntropy::Request]
|
|
89
|
+
def post_json(path, data, **)
|
|
90
|
+
post(path, 'application/json', JSON.dump(data), **)
|
|
55
91
|
end
|
|
56
92
|
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
# Makes an HTTP POST request to the test app with a
|
|
94
|
+
# "application/x-www-form-urlencoded" content type. The given data is
|
|
95
|
+
# converted to URL Encoded form format and sent as the request body.
|
|
96
|
+
#
|
|
97
|
+
# @param path [String] request path
|
|
98
|
+
# @param data [Hash] form data
|
|
99
|
+
# @return [Syntropy::Request]
|
|
100
|
+
def post_form(path, data, **)
|
|
101
|
+
post(path, 'application/x-www-form-urlencoded', URI.encode_www_form(data), **)
|
|
59
102
|
end
|
|
60
103
|
|
|
104
|
+
# Sets up a test instance.
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
61
107
|
def setup
|
|
62
108
|
raise 'Environment not set' if !@@env
|
|
63
109
|
|
|
110
|
+
Syntropy.load_config(@@env)
|
|
111
|
+
|
|
64
112
|
@machine = UM.new
|
|
65
113
|
@app = Syntropy::App.new(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
114
|
+
**@@env.merge(
|
|
115
|
+
machine: @machine,
|
|
116
|
+
test_mode: true
|
|
117
|
+
)
|
|
69
118
|
)
|
|
70
119
|
@test_harness = Syntropy::TestHarness.new(@app)
|
|
120
|
+
|
|
121
|
+
@db = load_module('/_lib/database', raise_on_missing: false)
|
|
122
|
+
@db&.migrate!
|
|
71
123
|
end
|
|
72
124
|
|
|
125
|
+
# Cleans up a test instance.
|
|
126
|
+
#
|
|
127
|
+
# @return [void]
|
|
73
128
|
def teardown
|
|
74
129
|
@machine = nil
|
|
75
130
|
@app = nil
|
|
@@ -77,18 +132,31 @@ module Syntropy
|
|
|
77
132
|
end
|
|
78
133
|
end
|
|
79
134
|
|
|
135
|
+
# TestHarness provides glue code for performing HTTP requests against a
|
|
136
|
+
# Syntropy app.
|
|
80
137
|
class TestHarness
|
|
138
|
+
# Initializes the test harness with the given app.
|
|
139
|
+
#
|
|
140
|
+
# @param app [Syntropy::App]
|
|
141
|
+
# @return [void]
|
|
81
142
|
def initialize(app)
|
|
82
143
|
@app = app
|
|
83
144
|
@app.raise_internal_server_error = true if @app.respond_to?(:raise_internal_server_error=)
|
|
84
145
|
end
|
|
85
146
|
|
|
147
|
+
# Perfrms a request against the associated app.
|
|
148
|
+
#
|
|
149
|
+
# @param headers [Hash] request headers
|
|
150
|
+
# @param body [String, nil] request body
|
|
151
|
+
# @return [Syntropy::Request]
|
|
86
152
|
def request(headers, body = nil)
|
|
87
153
|
req = mock_req(headers, body)
|
|
88
154
|
@app.call(req)
|
|
89
155
|
req
|
|
90
156
|
end
|
|
91
157
|
|
|
158
|
+
# Temporarily disables raising an exception in case of an internal server
|
|
159
|
+
# error while running the given block.
|
|
92
160
|
def no_raise_internal_server_error
|
|
93
161
|
return yield if !@app.respond_to?(:raise_internal_server_error=)
|
|
94
162
|
|
|
@@ -102,29 +170,50 @@ module Syntropy
|
|
|
102
170
|
|
|
103
171
|
private
|
|
104
172
|
|
|
173
|
+
# Creates a Syntropy request running on a mock adapter.
|
|
174
|
+
#
|
|
175
|
+
# @param headers [Hash] request headers
|
|
176
|
+
# @param body [String, nil] request body
|
|
177
|
+
# @return [Syntropy::Request]
|
|
105
178
|
def mock_req(headers, body = nil)
|
|
106
179
|
Syntropy::MockAdapter.mock(headers, body)
|
|
107
180
|
end
|
|
108
181
|
end
|
|
109
182
|
|
|
110
|
-
|
|
183
|
+
# Extensions to Syntropy::Request for testing.
|
|
184
|
+
module TestRequestExtensions
|
|
185
|
+
# Returns the response headers.
|
|
186
|
+
#
|
|
187
|
+
# @return [Hash]
|
|
111
188
|
def response_headers
|
|
112
189
|
adapter.response_headers
|
|
113
190
|
end
|
|
114
191
|
|
|
192
|
+
# Returns the response status
|
|
193
|
+
#
|
|
194
|
+
# @return [Integer]
|
|
115
195
|
def response_status
|
|
116
196
|
adapter.status
|
|
117
197
|
end
|
|
118
198
|
|
|
199
|
+
# Returns the response body
|
|
200
|
+
#
|
|
201
|
+
# @return [String, nil]
|
|
119
202
|
def response_body
|
|
120
203
|
adapter.response_body
|
|
121
204
|
end
|
|
122
205
|
|
|
206
|
+
# Parses the response body from JSON.
|
|
207
|
+
#
|
|
208
|
+
# @return [any] parsed JSON object
|
|
123
209
|
def response_json
|
|
124
210
|
raise if response_content_type != 'application/json'
|
|
125
211
|
JSON.parse(response_body)
|
|
126
212
|
end
|
|
127
213
|
|
|
214
|
+
# Returns the response content MIME type.
|
|
215
|
+
#
|
|
216
|
+
# @return [String, nil]
|
|
128
217
|
def response_content_type
|
|
129
218
|
ct = response_headers['Content-Type']
|
|
130
219
|
return nil if !ct
|
|
@@ -135,6 +224,10 @@ module Syntropy
|
|
|
135
224
|
m[1]
|
|
136
225
|
end
|
|
137
226
|
|
|
227
|
+
# Returns the cookie value for the given cookie name from the response.
|
|
228
|
+
#
|
|
229
|
+
# @param name [String, Symbol] cookie name
|
|
230
|
+
# @return [String, nil] cookie value
|
|
138
231
|
def response_cookie(name)
|
|
139
232
|
sc = response_headers['Set-Cookie']
|
|
140
233
|
return nil if !sc
|
|
@@ -145,4 +238,6 @@ module Syntropy
|
|
|
145
238
|
m[1]
|
|
146
239
|
end
|
|
147
240
|
end
|
|
241
|
+
|
|
242
|
+
Request.include TestRequestExtensions
|
|
148
243
|
end
|