syntropy 0.30.0 → 0.32.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/.gitignore +2 -0
- data/CHANGELOG.md +30 -0
- data/TODO.md +46 -1
- data/bin/syntropy +8 -86
- data/cmd/_banner.rb +16 -0
- data/cmd/console.rb +77 -0
- data/cmd/help.rb +12 -0
- data/cmd/serve.rb +95 -0
- data/cmd/test.rb +40 -0
- data/examples/{counter.rb → basic/counter.rb} +1 -1
- data/examples/{templates.rb → basic/templates.rb} +1 -1
- data/examples/blog/app/_layout/default.rb +11 -0
- data/examples/blog/app/_lib/post_store.rb +47 -0
- data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/blog/app/_setup.rb +4 -0
- data/examples/blog/app/index.rb +7 -0
- data/examples/blog/app/posts/[id]/edit.rb +33 -0
- data/examples/blog/app/posts/[id]/index.rb +58 -0
- data/examples/blog/app/posts/index.rb +38 -0
- data/examples/blog/app/posts/new.rb +29 -0
- data/examples/mcp-oauth/.ruby-version +1 -0
- data/examples/mcp-oauth/Gemfile +8 -0
- data/examples/mcp-oauth/README.md +128 -0
- data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
- data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
- data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
- data/examples/mcp-oauth/app/index.md +1 -0
- data/examples/mcp-oauth/app/mcp.rb +85 -0
- data/examples/mcp-oauth/app/oauth/authorize.rb +18 -0
- data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
- data/examples/mcp-oauth/app/oauth/register.rb +14 -0
- data/examples/mcp-oauth/app/oauth/token.rb +79 -0
- data/examples/mcp-oauth/app/signin.rb +85 -0
- data/examples/mcp-oauth/test/helper.rb +9 -0
- data/examples/mcp-oauth/test/test_app.rb +27 -0
- data/examples/mcp-oauth/test/test_oauth.rb +628 -0
- data/lib/syntropy/app.rb +34 -9
- data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
- data/lib/syntropy/applets/builtin/req.rb +1 -1
- data/lib/syntropy/db/connection_pool.rb +71 -0
- data/lib/syntropy/db/schema.rb +92 -0
- data/lib/syntropy/db/store.rb +31 -0
- data/lib/syntropy/dev_mode.rb +1 -1
- data/lib/syntropy/errors.rb +6 -0
- data/lib/syntropy/http/client.rb +43 -0
- data/lib/syntropy/http/client_connection.rb +36 -0
- data/lib/syntropy/http/io_extensions.rb +176 -0
- data/lib/syntropy/http/server.rb +5 -5
- data/lib/syntropy/http/{connection.rb → server_connection.rb} +15 -91
- data/lib/syntropy/http.rb +3 -1
- data/lib/syntropy/logger.rb +5 -1
- data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
- data/lib/syntropy/papercraft_extensions.rb +1 -1
- data/lib/syntropy/request/mock_adapter.rb +2 -0
- data/lib/syntropy/request/request_info.rb +22 -4
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/validation.rb +11 -5
- data/lib/syntropy/routing_tree.rb +2 -1
- data/lib/syntropy/test.rb +77 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +5 -23
- data/syntropy.gemspec +3 -3
- data/test/app/.well-known/foo.rb +3 -0
- data/test/app/_hook.rb +1 -1
- data/test/app/by_method.rb +9 -0
- data/test/app_setup/_setup.rb +7 -0
- data/test/app_setup/index.rb +1 -0
- data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
- data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
- data/test/helper.rb +1 -25
- data/test/schema/2026-01-02-foo.rb +12 -0
- data/test/schema/2026-05-30-bar.rb +7 -0
- data/test/test_app.rb +110 -70
- data/test/test_caching.rb +1 -1
- data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
- data/test/test_db_schema.rb +96 -0
- data/test/test_db_store.rb +24 -0
- data/test/test_http_client.rb +52 -0
- data/test/test_http_client_connection.rb +43 -0
- data/test/test_http_protocol.rb +250 -0
- data/test/{test_connection.rb → test_http_server_connection.rb} +39 -48
- data/test/test_json_api.rb +5 -5
- data/test/{test_module.rb → test_module_loader.rb} +31 -0
- data/test/{test_request_extensions.rb → test_request.rb} +153 -18
- data/test/test_routing_tree.rb +15 -3
- data/test/test_server.rb +9 -13
- metadata +84 -36
- data/lib/syntropy/connection_pool.rb +0 -61
- data/test/test_request_info.rb +0 -90
- /data/examples/{bad.rb → basic/bad.rb} +0 -0
- /data/examples/{card.rb → basic/card.rb} +0 -0
- /data/examples/{counter.js → basic/counter.js} +0 -0
- /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
- /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
- /data/examples/{index.md → basic/index.md} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fbf52e43aad7bcf2dc6259fd40dac6fb6f70d2215513ae89a5e72b120c4b8585
|
|
4
|
+
data.tar.gz: 7a7fd31a1e47bf999d64ad3216bb2c77345a4eb3753df6e2a51384870d9f764f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff3ac2fbb77785db855080dcd0302beb61332c067c30db6330540cda9a60a7ab72ad684b87772951e8498d5c6aa10b2cf57cce7f1a85932f95db889432c704a0
|
|
7
|
+
data.tar.gz: 84ee4aa581f46c84999109bb102d9c890f9dc45b2803a52f114357981edf9150b84c187ba3a62670691e3eb8629b6396bd8f1789f882857b1558089c198f2877
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
# 0.32.0 2026-06-01
|
|
2
|
+
|
|
3
|
+
- Ensure HTTP request body is consumed (skipped) before treating next request
|
|
4
|
+
- Fix `Request#auth_bearer_token`
|
|
5
|
+
- Add `syntropy console` CLI command
|
|
6
|
+
- Add `Module#http_methods` method for simpler REST controllers
|
|
7
|
+
- Add `App#setup_db` method
|
|
8
|
+
- Add `DB::Schema` for schema migrations with support for migration modules
|
|
9
|
+
- Add `DB::Store` class
|
|
10
|
+
- Rename `ConnectionPool` to `DB::ConnectionPool`
|
|
11
|
+
- Add support for setup file (`_setup.rb`)
|
|
12
|
+
- Remove escape_utils dependency
|
|
13
|
+
|
|
14
|
+
# 0.31.0 2026-05-29
|
|
15
|
+
|
|
16
|
+
- Add message kwarg to `Request#validate`
|
|
17
|
+
- Reraise internal server error in test mode
|
|
18
|
+
- Use strings rather than symbols for query keys
|
|
19
|
+
- Add content type validation
|
|
20
|
+
- Add syntropy CLI commands: serve, test, help
|
|
21
|
+
- Rename `#auto_refresh_watch!` to `#auto_refresh!`
|
|
22
|
+
- Use `zed` instead of `vscode` scheme for editor links (in HTML-rendered error
|
|
23
|
+
backtraces)
|
|
24
|
+
- Implement basic `HTTP::Client` API
|
|
25
|
+
- Rename `#json_response` to `#respond_json`, `#html_response` to
|
|
26
|
+
`#respond_html`
|
|
27
|
+
- Rename `HTTP::Connection` to `HTTP::ServerConnection`
|
|
28
|
+
- Refactor HTTP protocol into UM::IO extensions
|
|
29
|
+
- Fix routing for files/dirs starting with . (e.g. /.well-known/*)
|
|
30
|
+
|
|
1
31
|
# 0.30.0 2026-05-10
|
|
2
32
|
|
|
3
33
|
- Refactor HTTP modules
|
data/TODO.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
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
|
+
|
|
3
42
|
- [ ] Collection - treat directories and files as collections of data.
|
|
4
43
|
|
|
5
44
|
Kind of similar to the routing tree, but instead of routes it just takes a
|
|
@@ -39,9 +78,15 @@
|
|
|
39
78
|
- [ ] Website
|
|
40
79
|
- [v] Frontend part of JSON API
|
|
41
80
|
- [v] Auto-refresh page when file changes
|
|
81
|
+
- [ ] SQLite database capabilities
|
|
82
|
+
- [ ] Model API + tools
|
|
83
|
+
- [ ] Do we need/want migrations?
|
|
84
|
+
- [ ] Stores
|
|
85
|
+
- [ ] KV store (with TTL)
|
|
42
86
|
- [v] Examples
|
|
43
87
|
- [v] Reactive app - counter or some other simple app showing interaction with
|
|
44
88
|
server
|
|
89
|
+
- [ ] blog
|
|
45
90
|
|
|
46
91
|
## Testing facilities
|
|
47
92
|
|
|
@@ -49,7 +94,7 @@
|
|
|
49
94
|
- Routes
|
|
50
95
|
- Route responses
|
|
51
96
|
- Changes to state / DB
|
|
52
|
-
-
|
|
97
|
+
- Rendered HTML - presence of certain markup / elements / text
|
|
53
98
|
|
|
54
99
|
## Support for applets
|
|
55
100
|
|
data/bin/syntropy
CHANGED
|
@@ -1,93 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
require 'optparse'
|
|
4
|
+
require_relative '../cmd/_banner'
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
mount_path: '/',
|
|
9
|
-
banner: Syntropy::BANNER,
|
|
10
|
-
logger: true,
|
|
11
|
-
builtin_applet_path: '/.syntropy',
|
|
12
|
-
server_extensions: {
|
|
13
|
-
date: true,
|
|
14
|
-
name: 'Syntropy'
|
|
15
|
-
}
|
|
16
|
-
}
|
|
6
|
+
def cmd_fn(cmd)= File.join(__dir__, "../cmd/#{cmd}.rb")
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
cmd = ARGV.shift || 'help'
|
|
9
|
+
cmd = 'help' if cmd !~ /^[a-z]+$/
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
env[:bind] ||= []
|
|
24
|
-
env[:bind] << it
|
|
25
|
-
end
|
|
11
|
+
fn = cmd_fn(cmd)
|
|
12
|
+
fn = cmd_fn('help') if !File.file?(fn)
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
env[:logger] = nil
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
o.on('-d', '--dev', 'Development mode') do
|
|
33
|
-
env[:dev_mode] = true
|
|
34
|
-
env[:watch_files] = 0.1
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
o.on('-h', '--help', 'Show this help message') do
|
|
38
|
-
puts o
|
|
39
|
-
exit
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
|
43
|
-
p mount: path
|
|
44
|
-
env[:mount_path] = path
|
|
45
|
-
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
o.on('--no-builtin-applet', 'Do not mount builtin applet') do
|
|
49
|
-
env[:builtin_applet_path] = nil
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
o.on('--no-server-headers', 'Don\'t include Server and Date headers') do
|
|
53
|
-
env[:server_extensions] = nil
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
o.on('-v', '--version', 'Show version') do
|
|
57
|
-
require 'syntropy/version'
|
|
58
|
-
puts "Syntropy version #{Syntropy::VERSION}"
|
|
59
|
-
exit
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
RubyVM::YJIT.enable rescue nil
|
|
64
|
-
|
|
65
|
-
begin
|
|
66
|
-
parser.parse!
|
|
67
|
-
rescue StandardError => e
|
|
68
|
-
puts e.message
|
|
69
|
-
puts e.backtrace.join("\n")
|
|
70
|
-
exit
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
$syntropy_dev_mode = env[:dev_mode]
|
|
74
|
-
env[:root_dir] = (ARGV.shift || '.').gsub(/\/$/, '')
|
|
75
|
-
|
|
76
|
-
if !File.directory?(env[:root_dir])
|
|
77
|
-
puts "#{File.expand_path(env[:root_dir])} Not a directory"
|
|
78
|
-
exit
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
puts env[:banner] if env[:banner]
|
|
82
|
-
env[:banner] = false
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# We set Syntropy.machine so we can reference it from anywhere
|
|
86
|
-
env[:machine] = Syntropy.machine = UM.new
|
|
87
|
-
env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
|
|
88
|
-
|
|
89
|
-
require 'syntropy/version'
|
|
90
|
-
require 'syntropy/dev_mode' if env[:dev_mode]
|
|
91
|
-
|
|
92
|
-
app = Syntropy::App.load(env)
|
|
93
|
-
Syntropy.run(env) { app.call(it) }
|
|
14
|
+
puts SYNTROPY_BANNER
|
|
15
|
+
require(fn)
|
data/cmd/_banner.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
green = "\e[32m"
|
|
4
|
+
clear = "\e[0m"
|
|
5
|
+
yellow = "\e[33m"
|
|
6
|
+
|
|
7
|
+
SYNTROPY_BANNER =
|
|
8
|
+
"\n"\
|
|
9
|
+
" #{green}\n"\
|
|
10
|
+
" #{green} ooo\n"\
|
|
11
|
+
" #{green}ooooo\n"\
|
|
12
|
+
" #{green} ooo vvv #{clear}Syntropy - a web framework for Ruby\n"\
|
|
13
|
+
" #{green} o vvvvv #{clear}--------------------------------------\n"\
|
|
14
|
+
" #{green} #{yellow}|#{green} vvv o #{clear}https://github.com/digital-fabric/syntropy\n"\
|
|
15
|
+
" #{green} :#{yellow}|#{green}:::#{yellow}|#{green}::#{yellow}|#{green}:\n"\
|
|
16
|
+
"#{yellow}++++++++++++++++++++++++++++++++++++++++++++++++++++++++++#{clear}\n\n"
|
data/cmd/console.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../lib/syntropy'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
env = {
|
|
7
|
+
mount_path: '/',
|
|
8
|
+
logger: true,
|
|
9
|
+
builtin_applet_path: '/.syntropy',
|
|
10
|
+
watch_files: true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
parser = OptionParser.new do |o|
|
|
14
|
+
o.banner = 'Usage: syntropy serve [options] DIR'
|
|
15
|
+
|
|
16
|
+
o.on('-h', '--help', 'Show this help message') do
|
|
17
|
+
puts o
|
|
18
|
+
exit
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
|
22
|
+
p mount: path
|
|
23
|
+
env[:mount_path] = path
|
|
24
|
+
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
o.on('--no-builtin-applet', 'Do not mount builtin applet') do
|
|
28
|
+
env[:builtin_applet_path] = nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
RubyVM::YJIT.enable rescue nil
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
parser.parse!
|
|
36
|
+
rescue OptionParser::InvalidOption
|
|
37
|
+
puts parser
|
|
38
|
+
exit
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
p e
|
|
41
|
+
puts e.message
|
|
42
|
+
puts e.backtrace.join("\n")
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
$syntropy_dev_mode = env[:dev_mode]
|
|
47
|
+
env[:root_dir] = (ARGV.shift || '.').gsub(/\/$/, '')
|
|
48
|
+
|
|
49
|
+
if !File.directory?(env[:root_dir])
|
|
50
|
+
puts "#{File.expand_path(env[:root_dir])} Not a directory"
|
|
51
|
+
exit
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts env[:banner] if env[:banner]
|
|
55
|
+
env[:banner] = false
|
|
56
|
+
|
|
57
|
+
# We set Syntropy.machine so we can reference it from anywhere
|
|
58
|
+
env[:machine] = Syntropy.machine = UM.new
|
|
59
|
+
env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
|
|
60
|
+
|
|
61
|
+
@app = Syntropy::App.load(env)
|
|
62
|
+
@env = env
|
|
63
|
+
@machine = env[:machine]
|
|
64
|
+
@connection_pool = @app.connection_pool if @app.respond_to?(:connection_pool)
|
|
65
|
+
@schema = @app.schema if @app.respond_to?(:schema)
|
|
66
|
+
@module_loader = @app.module_loader
|
|
67
|
+
|
|
68
|
+
require 'uringmachine/fiber_scheduler'
|
|
69
|
+
@scheduler = UM::FiberScheduler.new(@machine)
|
|
70
|
+
Fiber.set_scheduler(@scheduler)
|
|
71
|
+
|
|
72
|
+
def import(ref)
|
|
73
|
+
@module_loader.load(ref)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
require 'irb'
|
|
77
|
+
IRB.start
|
data/cmd/help.rb
ADDED
data/cmd/serve.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../lib/syntropy'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
env = {
|
|
7
|
+
mount_path: '/',
|
|
8
|
+
logger: true,
|
|
9
|
+
builtin_applet_path: '/.syntropy',
|
|
10
|
+
server_extensions: {
|
|
11
|
+
date: true,
|
|
12
|
+
name: 'Syntropy'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
parser = OptionParser.new do |o|
|
|
17
|
+
o.banner = 'Usage: syntropy serve [options] DIR'
|
|
18
|
+
|
|
19
|
+
o.on('-b', '--bind BIND', String,
|
|
20
|
+
'Bind address (default: http://0.0.0.0:1234). You can specify this flag multiple times to bind to multiple addresses.') do
|
|
21
|
+
env[:bind] ||= []
|
|
22
|
+
env[:bind] << it
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
o.on('-s', '--silent', 'Silent mode') do
|
|
26
|
+
env[:banner] = nil
|
|
27
|
+
env[:logger] = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
o.on('-d', '--dev', 'Development mode') do
|
|
31
|
+
env[:dev_mode] = true
|
|
32
|
+
env[:watch_files] = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
o.on('-h', '--help', 'Show this help message') do
|
|
36
|
+
puts o
|
|
37
|
+
exit
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
|
41
|
+
p mount: path
|
|
42
|
+
env[:mount_path] = path
|
|
43
|
+
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
o.on('--no-builtin-applet', 'Do not mount builtin applet') do
|
|
47
|
+
env[:builtin_applet_path] = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
o.on('--no-server-headers', 'Don\'t include Server and Date headers') do
|
|
51
|
+
env[:server_extensions] = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
o.on('-v', '--version', 'Show version') do
|
|
55
|
+
require 'syntropy/version'
|
|
56
|
+
puts "Syntropy version #{Syntropy::VERSION}"
|
|
57
|
+
exit
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
RubyVM::YJIT.enable rescue nil
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
parser.parse!
|
|
65
|
+
rescue OptionParser::InvalidOption
|
|
66
|
+
puts parser
|
|
67
|
+
exit
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
p e
|
|
70
|
+
puts e.message
|
|
71
|
+
puts e.backtrace.join("\n")
|
|
72
|
+
exit
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
$syntropy_dev_mode = env[:dev_mode]
|
|
76
|
+
env[:root_dir] = (ARGV.shift || '.').gsub(/\/$/, '')
|
|
77
|
+
|
|
78
|
+
if !File.directory?(env[:root_dir])
|
|
79
|
+
puts "#{File.expand_path(env[:root_dir])} Not a directory"
|
|
80
|
+
exit
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
puts env[:banner] if env[:banner]
|
|
84
|
+
env[:banner] = false
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# We set Syntropy.machine so we can reference it from anywhere
|
|
88
|
+
env[:machine] = Syntropy.machine = UM.new
|
|
89
|
+
env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
|
|
90
|
+
|
|
91
|
+
require 'syntropy/version'
|
|
92
|
+
require 'syntropy/dev_mode' if env[:dev_mode]
|
|
93
|
+
|
|
94
|
+
app = Syntropy::App.load(env)
|
|
95
|
+
Syntropy.run(env) { app.call(it) }
|
data/cmd/test.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
env = {}
|
|
4
|
+
argv_copy = ARGV.dup
|
|
5
|
+
case ARGV[0]
|
|
6
|
+
when '-w', '--watch'
|
|
7
|
+
env[:watch_mode] = true
|
|
8
|
+
ARGV.shift
|
|
9
|
+
when '-h', '--help'
|
|
10
|
+
puts <<~MSG
|
|
11
|
+
Usage: syntropy test [options] [minitest options]
|
|
12
|
+
-w, --watch Rerun tests on file system changes
|
|
13
|
+
-h, --help Show this help message
|
|
14
|
+
MSG
|
|
15
|
+
exit
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require_relative '../lib/syntropy'
|
|
19
|
+
require_relative '../lib/syntropy/test'
|
|
20
|
+
|
|
21
|
+
$stdout.sync = true
|
|
22
|
+
$stderr.sync = true
|
|
23
|
+
|
|
24
|
+
Dir.glob("./test/test_*.rb").each { require(it) }
|
|
25
|
+
|
|
26
|
+
def watch_for_file_changes
|
|
27
|
+
m = UM.new
|
|
28
|
+
puts "Waiting for file changes in #{FileUtils.pwd}"
|
|
29
|
+
m.file_watch(FileUtils.pwd, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) {
|
|
30
|
+
puts "Detected changes to #{it[:fn]}, restarting"
|
|
31
|
+
break
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Minitest.run ARGV
|
|
36
|
+
if env[:watch_mode]
|
|
37
|
+
puts
|
|
38
|
+
watch_for_file_changes
|
|
39
|
+
exec("ruby", __FILE__, *argv_copy)
|
|
40
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class PostStore < Syntropy::DB::Store
|
|
2
|
+
# @return [Integer] post id
|
|
3
|
+
def create(title, body)
|
|
4
|
+
query_single_value <<~SQL, title:, body:
|
|
5
|
+
insert into posts (title, body)
|
|
6
|
+
values (:title, :body)
|
|
7
|
+
returning id;
|
|
8
|
+
SQL
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [void]
|
|
12
|
+
def update(id, title, body)
|
|
13
|
+
execute <<~SQL, id:, title:, body:
|
|
14
|
+
update posts
|
|
15
|
+
set title = :title, body = :body
|
|
16
|
+
where id = :id
|
|
17
|
+
SQL
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [void]
|
|
21
|
+
def delete(id)
|
|
22
|
+
execute <<~SQL, id:
|
|
23
|
+
delete from posts
|
|
24
|
+
where id = :id
|
|
25
|
+
SQL
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# return [Hash]
|
|
29
|
+
def get(id)
|
|
30
|
+
query_single_row <<~SQL, id:
|
|
31
|
+
select id, title, body
|
|
32
|
+
from posts
|
|
33
|
+
where id = :id
|
|
34
|
+
SQL
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# return [Array<Hash>]
|
|
38
|
+
def get_all
|
|
39
|
+
query <<~SQL
|
|
40
|
+
select id, title, body
|
|
41
|
+
from posts
|
|
42
|
+
order by id desc
|
|
43
|
+
SQL
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
export PostStore.new(@app.connection_pool)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@post_store = import '/_lib/post_store'
|
|
2
|
+
@layout = import '/_layout/default'
|
|
3
|
+
|
|
4
|
+
export http_methods
|
|
5
|
+
|
|
6
|
+
def get(req)
|
|
7
|
+
id = req.route_params['id'].to_i
|
|
8
|
+
post = @post_store.get(id)
|
|
9
|
+
raise Syntropy::Error.not_found if !post
|
|
10
|
+
|
|
11
|
+
req.respond_html(
|
|
12
|
+
@template.render(post:)
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@template = @layout.apply { |post:, **props|
|
|
17
|
+
h1 "Edit blog post"
|
|
18
|
+
div {
|
|
19
|
+
form(action: "/posts/#{post[:id]}", method: 'post') {
|
|
20
|
+
div {
|
|
21
|
+
label 'Title', for: 'title'
|
|
22
|
+
input name: 'title', type: 'text', value: post[:title]
|
|
23
|
+
}
|
|
24
|
+
div {
|
|
25
|
+
label 'Body', for: 'body'
|
|
26
|
+
textarea post[:body], name: 'body', rows: 5
|
|
27
|
+
}
|
|
28
|
+
div {
|
|
29
|
+
button 'Submit', type: 'submit'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
@post_store = import '/_lib/post_store'
|
|
2
|
+
@layout = import '/_layout/default'
|
|
3
|
+
|
|
4
|
+
export http_methods
|
|
5
|
+
|
|
6
|
+
def get(req)
|
|
7
|
+
id = req.route_params['id'].to_i
|
|
8
|
+
post = @post_store.get(id)
|
|
9
|
+
raise Syntropy::Error.not_found if !post
|
|
10
|
+
|
|
11
|
+
req.respond_html(
|
|
12
|
+
@template.render(post:)
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def post(req)
|
|
17
|
+
data = req.get_form_data
|
|
18
|
+
return delete(req) if data['method'] == 'delete'
|
|
19
|
+
|
|
20
|
+
id = req.route_params['id'].to_i
|
|
21
|
+
title = req.validate(data['title'], String, /.+/)
|
|
22
|
+
body = req.validate(data['body'], String, /.+/)
|
|
23
|
+
|
|
24
|
+
updated = @post_store.update(id, title, body)
|
|
25
|
+
raise BadRequestError, "Failed to update post" if updated != 1
|
|
26
|
+
|
|
27
|
+
req.redirect "/posts/#{id}", Syntropy::HTTP::SEE_OTHER
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def delete(req)
|
|
31
|
+
id = req.route_params['id'].to_i
|
|
32
|
+
|
|
33
|
+
deleted = @post_store.delete(id)
|
|
34
|
+
raise BadRequestError, "Failed to delete post" if deleted != 1
|
|
35
|
+
|
|
36
|
+
req.redirect "/posts", Syntropy::HTTP::SEE_OTHER
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
@template = @layout.apply { |post:, **props|
|
|
40
|
+
h1 "My blog"
|
|
41
|
+
div {
|
|
42
|
+
h2 {
|
|
43
|
+
a post[:title]
|
|
44
|
+
}
|
|
45
|
+
p post[:body]
|
|
46
|
+
}
|
|
47
|
+
p {
|
|
48
|
+
a "Edit", href: "/posts/#{post[:id]}/edit"
|
|
49
|
+
span '|'
|
|
50
|
+
a "Back to posts", href: '/posts'
|
|
51
|
+
}
|
|
52
|
+
div {
|
|
53
|
+
form(method: 'post') {
|
|
54
|
+
input type: 'hidden', name: 'method', value: 'delete'
|
|
55
|
+
button 'Delete this post', name: 'delete', type: 'submit'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|