syntropy 0.32.0 → 0.33.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/TODO.md +0 -39
- data/cmd/serve.rb +0 -2
- data/cmd/test.rb +76 -20
- data/examples/blog/app/posts/[id]/index.rb +4 -1
- data/examples/blog/app/posts/index.rb +3 -1
- data/examples/mcp-oauth/test/test_app.rb +2 -20
- data/examples/mcp-oauth/test/test_oauth.rb +93 -217
- data/lib/syntropy/http/server_connection.rb +15 -9
- data/lib/syntropy/module_loader.rb +5 -4
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/session.rb +113 -0
- data/lib/syntropy/request.rb +9 -0
- data/lib/syntropy/test.rb +72 -1
- data/lib/syntropy/version.rb +1 -1
- data/syntropy.gemspec +1 -0
- data/test/test_http_server_connection.rb +8 -5
- data/test/test_request_session.rb +254 -0
- metadata +17 -2
- data/examples/mcp-oauth/test/helper.rb +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a39afb5d39d1a1c9b95e2e242b9ce55144ba6923ad4b0addee3c6c005d314ead
|
|
4
|
+
data.tar.gz: 0b067b50a4f27381ba495a32d8c8e471e4a64374d6eee3c62d25fb30aaa10a24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9768274a2d09c8f4068625006660403e98655cfa7d7d742ed75f3ce0e31abfc0410f301d16d8d8910fcc94d7947008238a730b19b514342cbb67dac8669a4e76
|
|
7
|
+
data.tar.gz: 76a2d90674f7ebc34f8162e3435d81edb1ca56022ef7d490bcc6fcfa0041529f1ace321233451dd7df40867b05eb83327984634461f0986dd650db3e0913af6c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# 0.33.0 2026-06-02
|
|
2
|
+
|
|
3
|
+
- Fix `ModuleLoader` to load a module only once
|
|
4
|
+
- Improve testing tools, add `Syntropy::Test` class
|
|
5
|
+
- Implement `Request#session`, `Request#flash`
|
|
6
|
+
- Improve `Request#set_cookie`
|
|
7
|
+
|
|
1
8
|
# 0.32.0 2026-06-01
|
|
2
9
|
|
|
3
10
|
- Ensure HTTP request body is consumed (skipped) before treating next request
|
data/TODO.md
CHANGED
|
@@ -1,44 +1,5 @@
|
|
|
1
1
|
## Immediate
|
|
2
2
|
|
|
3
|
-
- [ ] Session
|
|
4
|
-
|
|
5
|
-
https://guides.rubyonrails.org/action_controller_overview.html#session
|
|
6
|
-
|
|
7
|
-
We want something that offers the same features as in Ruby on Rails. A storage
|
|
8
|
-
space for session metadata which can include a user_id, flash messages etc.
|
|
9
|
-
|
|
10
|
-
- The session is attached to the request, and is valid for the browser
|
|
11
|
-
session.
|
|
12
|
-
- The session is a KV store. It can be used to store any data relevant to the
|
|
13
|
-
user's browser session.
|
|
14
|
-
- Each session has a unique ID and that ID is passed to the browser as a
|
|
15
|
-
non-persistent cookie.
|
|
16
|
-
- A session expires if not used (e.g. after 7 days)
|
|
17
|
-
- Session storage either in memory or in DB
|
|
18
|
-
- The session is generated (and session cookie set) upon first write to
|
|
19
|
-
session.
|
|
20
|
-
- Session `Set-Cookie` header should be injected into the HTTP response.
|
|
21
|
-
- The entire session info can be stored in a cookie, provided it does not
|
|
22
|
-
exceed 4KB.
|
|
23
|
-
|
|
24
|
-
```ruby
|
|
25
|
-
req.session[:flash] = 'Title cannot be empty!'
|
|
26
|
-
req.redirect '/blah'
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
- [ ] Flash messages
|
|
30
|
-
|
|
31
|
-
- Flash messages are a sub-feature of session storage and are used to relay
|
|
32
|
-
messages from one request to the next in the same session.
|
|
33
|
-
- Flash messages have more complex semantics:
|
|
34
|
-
- There can be more than one.
|
|
35
|
-
- They have types - notice, alert, etc.
|
|
36
|
-
- They normally disappear on the next request (i.e. the session cookie
|
|
37
|
-
should be updated in the response with Set-Cookie).
|
|
38
|
-
- But, you can keep them for the next request with `req.session.flash.keep`
|
|
39
|
-
- The flash messages could be used for the current request with
|
|
40
|
-
`req.session.flash.now`
|
|
41
|
-
|
|
42
3
|
- [ ] Collection - treat directories and files as collections of data.
|
|
43
4
|
|
|
44
5
|
Kind of similar to the routing tree, but instead of routes it just takes a
|
data/cmd/serve.rb
CHANGED
|
@@ -38,7 +38,6 @@ parser = OptionParser.new do |o|
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
|
41
|
-
p mount: path
|
|
42
41
|
env[:mount_path] = path
|
|
43
42
|
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
|
44
43
|
end
|
|
@@ -83,7 +82,6 @@ end
|
|
|
83
82
|
puts env[:banner] if env[:banner]
|
|
84
83
|
env[:banner] = false
|
|
85
84
|
|
|
86
|
-
|
|
87
85
|
# We set Syntropy.machine so we can reference it from anywhere
|
|
88
86
|
env[:machine] = Syntropy.machine = UM.new
|
|
89
87
|
env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
|
data/cmd/test.rb
CHANGED
|
@@ -1,17 +1,70 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
pwd = FileUtils.pwd
|
|
7
|
+
env = {
|
|
8
|
+
root_dir: File.join(pwd, 'app'),
|
|
9
|
+
test_dir: File.join(pwd, 'test'),
|
|
10
|
+
mount_path: '/'
|
|
11
|
+
}
|
|
12
|
+
MINITEST_ARGV = []
|
|
13
|
+
|
|
14
|
+
parser = OptionParser.new do |o|
|
|
15
|
+
o.banner = 'Usage: syntropy test [options]'
|
|
16
|
+
|
|
17
|
+
o.on('-h', '--help', 'Show this help message') do
|
|
18
|
+
puts o
|
|
19
|
+
exit
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
|
23
|
+
env[:mount_path] = path
|
|
24
|
+
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
o.on('-w', '--watch', 'Watch for file changes') do
|
|
28
|
+
env[:watch_mode] = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
o.on('-a', '--app PATH', 'Set app root (default: ./app)') do |path|
|
|
32
|
+
env[:root_dir] = path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
o.on('-t', '--test PATH', 'Set test root (default: ./test)') do |path|
|
|
36
|
+
env[:test_dir] = path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
o.on('-n', '--name NAME', 'Specify test to run') do |name|
|
|
40
|
+
MINITEST_ARGV << '--name' << name
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
o.on('-V', '--verbose', 'Verbose test output') do
|
|
44
|
+
MINITEST_ARGV << '--verbose'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
o.on('-s', '--seed SEED', 'Specify random seed') do |seed|
|
|
48
|
+
MINITEST_ARGV << '--seed' << seed
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
o.on('-v', '--version', 'Show version') do
|
|
52
|
+
require 'syntropy/version'
|
|
53
|
+
puts "Syntropy version #{Syntropy::VERSION}"
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
4
58
|
argv_copy = ARGV.dup
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
MSG
|
|
59
|
+
begin
|
|
60
|
+
parser.parse!
|
|
61
|
+
rescue OptionParser::InvalidOption
|
|
62
|
+
puts parser
|
|
63
|
+
exit
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
p e
|
|
66
|
+
puts e.message
|
|
67
|
+
puts e.backtrace.join("\n")
|
|
15
68
|
exit
|
|
16
69
|
end
|
|
17
70
|
|
|
@@ -21,20 +74,23 @@ require_relative '../lib/syntropy/test'
|
|
|
21
74
|
$stdout.sync = true
|
|
22
75
|
$stderr.sync = true
|
|
23
76
|
|
|
24
|
-
Dir.glob("
|
|
77
|
+
Dir.glob("#{env[:test_dir]}/test_*.rb").each { require(it) }
|
|
25
78
|
|
|
26
|
-
def watch_for_file_changes
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
puts "Detected changes to #{it[:fn]}, restarting"
|
|
79
|
+
def watch_for_file_changes(machine)
|
|
80
|
+
machine.write(UM::STDOUT_FILENO, "Waiting for file changes in #{FileUtils.pwd}\n")
|
|
81
|
+
machine.file_watch(FileUtils.pwd, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) {
|
|
82
|
+
machine.write(UM::STDOUT_FILENO, "File changed: #{it[:fn]}\n")
|
|
31
83
|
break
|
|
32
84
|
}
|
|
33
85
|
end
|
|
34
86
|
|
|
35
|
-
|
|
87
|
+
Syntropy::Test.env=(env)
|
|
88
|
+
Minitest.run MINITEST_ARGV
|
|
89
|
+
|
|
36
90
|
if env[:watch_mode]
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
m = UM.new(size: 4)
|
|
92
|
+
m.write(UM::STDOUT_FILENO, "\n")
|
|
93
|
+
trap('SIGINT') { m.write(UM::STDOUT_FILENO, "\n"); exit! }
|
|
94
|
+
watch_for_file_changes(m)
|
|
39
95
|
exec("ruby", __FILE__, *argv_copy)
|
|
40
96
|
end
|
|
@@ -9,7 +9,7 @@ def get(req)
|
|
|
9
9
|
raise Syntropy::Error.not_found if !post
|
|
10
10
|
|
|
11
11
|
req.respond_html(
|
|
12
|
-
@template.render(post:)
|
|
12
|
+
@template.render(post:, req:)
|
|
13
13
|
)
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -24,6 +24,7 @@ def post(req)
|
|
|
24
24
|
updated = @post_store.update(id, title, body)
|
|
25
25
|
raise BadRequestError, "Failed to update post" if updated != 1
|
|
26
26
|
|
|
27
|
+
req.flash[:notice] = 'Post was successfully updated.'
|
|
27
28
|
req.redirect "/posts/#{id}", Syntropy::HTTP::SEE_OTHER
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -33,11 +34,13 @@ def delete(req)
|
|
|
33
34
|
deleted = @post_store.delete(id)
|
|
34
35
|
raise BadRequestError, "Failed to delete post" if deleted != 1
|
|
35
36
|
|
|
37
|
+
req.flash[:notice] = 'Post was successfully destroyed.'
|
|
36
38
|
req.redirect "/posts", Syntropy::HTTP::SEE_OTHER
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
@template = @layout.apply { |post:, **props|
|
|
40
42
|
h1 "My blog"
|
|
43
|
+
p props[:req]&.flash[:notice], style: 'color: green'
|
|
41
44
|
div {
|
|
42
45
|
h2 {
|
|
43
46
|
a post[:title]
|
|
@@ -6,7 +6,7 @@ export http_methods
|
|
|
6
6
|
def get(req)
|
|
7
7
|
posts = @post_store.get_all
|
|
8
8
|
req.respond_html(
|
|
9
|
-
@template.render(posts:)
|
|
9
|
+
@template.render(posts:, req:)
|
|
10
10
|
)
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -16,11 +16,13 @@ def post(req)
|
|
|
16
16
|
body = req.validate(data['body'], String, /.+/)
|
|
17
17
|
id = @post_store.create(title, body)
|
|
18
18
|
|
|
19
|
+
req.flash[:notice] = 'Post was successfully created.'
|
|
19
20
|
req.redirect("posts/#{id}")
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
@template = @layout.apply { |**props|
|
|
23
24
|
h1 "My blog"
|
|
25
|
+
p props[:req]&.flash[:notice], style: 'color: green'
|
|
24
26
|
props[:posts].each { |post|
|
|
25
27
|
div {
|
|
26
28
|
h2 {
|
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class AppTest < Minitest::Test
|
|
6
|
-
APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
|
|
7
|
-
HTTP = Syntropy::HTTP
|
|
8
|
-
|
|
9
|
-
def setup
|
|
10
|
-
@machine = UM.new
|
|
11
|
-
@app = Syntropy::App.new(
|
|
12
|
-
root_dir: APP_ROOT,
|
|
13
|
-
mount_path: '/',
|
|
14
|
-
machine: @machine
|
|
15
|
-
)
|
|
16
|
-
@test_harness = Syntropy::TestHarness.new(@app)
|
|
17
|
-
end
|
|
18
|
-
|
|
3
|
+
class AppTest < Syntropy::Test
|
|
19
4
|
def test_root
|
|
20
|
-
req =
|
|
21
|
-
':method' => 'GET',
|
|
22
|
-
':path' => '/'
|
|
23
|
-
)
|
|
5
|
+
req = get('/')
|
|
24
6
|
assert_equal HTTP::OK, req.response_status
|
|
25
7
|
assert_match /Syntropy/, req.response_body
|
|
26
8
|
end
|