syntropy 0.36.0 → 0.38.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 +23 -0
- data/Gemfile +4 -0
- data/TODO.md +4 -0
- data/bin/syntropy +12 -2
- data/cmd/help.rb +4 -0
- data/cmd/new/template/.gitignore +2 -1
- data/cmd/new/template/config/Caddyfile +5 -0
- data/cmd/new/template/docker-compose.yml +28 -3
- data/cmd/new.rb +7 -1
- data/cmd/serve.rb +3 -1
- data/cmd/version.rb +14 -0
- data/examples/basic/counter_api.rb +1 -1
- data/examples/blog/app/posts/[id]/edit.rb +1 -1
- data/examples/blog/app/posts/[id]/index.rb +1 -1
- data/examples/blog/app/posts/index.rb +1 -1
- data/examples/blog/app/posts/new.rb +1 -1
- data/examples/github/app/[org]/[repo]/index.rb +0 -0
- data/examples/github/app/[org]/[repo]/issues/[id].rb +0 -0
- data/examples/github/app/[org]/index.rb +0 -0
- data/examples/github/app/collections.rb +0 -0
- data/examples/github/app/explore.rb +0 -0
- data/examples/github/app/index.rb +0 -0
- data/lib/syntropy/app.rb +6 -2
- data/lib/syntropy/controller_extensions.rb +136 -0
- data/lib/syntropy/http/io_extensions.rb +9 -0
- data/lib/syntropy/http/server_connection.rb +1 -0
- data/lib/syntropy/json_api.rb +5 -0
- data/lib/syntropy/module_loader.rb +46 -42
- data/lib/syntropy/routing_tree.rb +14 -14
- data/lib/syntropy/storage/schema.rb +3 -3
- data/lib/syntropy/test.rb +29 -11
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +3 -6
- data/test/bm_router_proc.rb +14 -15
- data/test/fixtures/app/_lib/klass.rb +1 -1
- data/test/fixtures/app/api+.rb +1 -1
- data/test/fixtures/app/bad_mod_arity.rb +3 -0
- data/test/fixtures/app/by_method.rb +1 -1
- data/test/fixtures/app/post_ct.rb +1 -1
- data/test/fixtures/app_errors/_error.rb +3 -0
- data/test/fixtures/app_errors/foo/_error.rb +3 -0
- data/test/fixtures/app_errors/foo/bar/_error.rb +3 -0
- data/test/fixtures/app_errors/foo/bar/baz/index.rb +3 -0
- data/test/fixtures/app_errors/foo/bar/index.rb +3 -0
- data/test/fixtures/app_errors/foo/index.rb +3 -0
- data/test/fixtures/app_errors/index.rb +3 -0
- data/test/fixtures/app_hooks/_hook.rb +4 -0
- data/test/fixtures/app_hooks/foo/_hook.rb +4 -0
- data/test/fixtures/app_hooks/foo/bar/_hook.rb +4 -0
- data/test/fixtures/app_hooks/foo/bar/baz/_hook.rb +4 -0
- data/test/fixtures/app_hooks/foo/bar/baz/index.rb +3 -0
- data/test/fixtures/app_hooks/foo/bar/index.rb +3 -0
- data/test/fixtures/app_hooks/foo/index.rb +3 -0
- data/test/fixtures/app_hooks/index.rb +3 -0
- data/test/fixtures/app_multi_site/_site.rb +1 -1
- data/test/fixtures/controllers/by_host/bar.com/index.rb +3 -0
- data/test/fixtures/controllers/by_host/foo.com/index.rb +3 -0
- data/test/fixtures/controllers/by_host_dir.rb +1 -0
- data/test/fixtures/controllers/by_host_dir_map.rb +4 -0
- data/test/fixtures/controllers/by_host_map.rb +4 -0
- data/test/fixtures/controllers/by_http_method.rb +9 -0
- data/test/fixtures/controllers/jsonrpc_endpoint.rb +0 -0
- data/test/test_app.rb +86 -1
- data/test/test_controller.rb +71 -0
- data/test/test_http_protocol.rb +54 -0
- data/test/test_module_loader.rb +43 -5
- data/test/test_routing_tree.rb +1 -0
- data/test/test_test.rb +1 -1
- metadata +34 -2
- data/lib/syntropy/utils.rb +0 -87
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85989e43beae58a319d108e1c8cf42f8265fdfaa0675c0fa2b0b8c8df5dbfbd1
|
|
4
|
+
data.tar.gz: cd2dd5db7add2b3e3aa74f2a3fa02da3c1896b808a46be16b088e264875aec10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cd0fb5d085456bc8d51db049fecaf11f000071cde6b19dd224c7ec422e1fb749be3ba92dfd5b8a43e3e95313f99b1ffed5f926ca37abd5377e2badd249576f3
|
|
7
|
+
data.tar.gz: c3a2b789fe2c9cfbff79b2245381f3ee76bad2e905e3f8408df2666af1495b51f93988d7085c1957330b194237f366e0999b43eb25b8e0e42954f07146aaa5cb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
# 0.38.0 2026-06-13
|
|
2
|
+
|
|
3
|
+
- Reimplement controller extensions: `dispatch_by_host`,
|
|
4
|
+
`dispatch_by_http_method`
|
|
5
|
+
- Fix middleware composition
|
|
6
|
+
- Add CLI command shortcuts
|
|
7
|
+
- Rename `Syntropy::Module` to `Syntropy::ModuleContext`
|
|
8
|
+
|
|
9
|
+
# 0.37.0 2026-06-07
|
|
10
|
+
|
|
11
|
+
- Call `IO#clear` before closing server connection
|
|
12
|
+
- syntropy new:
|
|
13
|
+
- Remove socket mapping for backend in docker-compose.yml
|
|
14
|
+
- Add caddy reverse proxy to template
|
|
15
|
+
- Add overwrite confirmation, better file copying
|
|
16
|
+
- Verify storage module `migrate!` method exists
|
|
17
|
+
- Fix `set_schema_version` for usage in PG DB
|
|
18
|
+
- Do not raise exception on missing config module
|
|
19
|
+
- Do not convert class export value to class instance, allow exporting a
|
|
20
|
+
class
|
|
21
|
+
- Fix HTTP protocol error when pipelining post requests with empty body
|
|
22
|
+
- Setup fiber scheduler when running server
|
|
23
|
+
|
|
1
24
|
# 0.36.0 2026-06-04
|
|
2
25
|
|
|
3
26
|
- Rename `DB` to `Storage`
|
data/Gemfile
CHANGED
data/TODO.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## Immediate
|
|
2
2
|
|
|
3
|
+
- [ ] Controllers
|
|
4
|
+
- [ ] Streamline names of ready-made control methods:
|
|
5
|
+
- [ ] dispatch_json_rpc
|
|
6
|
+
|
|
3
7
|
- [ ] Collection - treat directories and files as collections of data.
|
|
4
8
|
|
|
5
9
|
Kind of similar to the routing tree, but instead of routes it just takes a
|
data/bin/syntropy
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require_relative '../cmd/_banner'
|
|
5
|
-
|
|
6
4
|
def cmd_fn(cmd)= File.join(__dir__, "../cmd/#{cmd}.rb")
|
|
7
5
|
|
|
8
6
|
cmd = ARGV.shift || 'help'
|
|
9
7
|
cmd = 'help' if cmd !~ /^[a-z]+$/
|
|
10
8
|
|
|
9
|
+
SHORTCUTS = {
|
|
10
|
+
'c' => 'console',
|
|
11
|
+
'n' => 'new',
|
|
12
|
+
's' => 'serve',
|
|
13
|
+
't' => 'test',
|
|
14
|
+
'v' => 'version'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (target_cmd = SHORTCUTS[cmd])
|
|
18
|
+
cmd = target_cmd
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
fn = cmd_fn(cmd)
|
|
12
22
|
fn = cmd_fn('help') if !File.file?(fn)
|
|
13
23
|
|
data/cmd/help.rb
CHANGED
data/cmd/new/template/.gitignore
CHANGED
|
@@ -7,17 +7,18 @@ services:
|
|
|
7
7
|
- seccomp:unconfined
|
|
8
8
|
volumes:
|
|
9
9
|
- .:/syntropy
|
|
10
|
-
ports:
|
|
11
|
-
- "1234:1234"
|
|
12
10
|
stop_signal: SIGINT
|
|
13
11
|
stop_grace_period: 10s
|
|
14
|
-
restart:
|
|
12
|
+
restart: unless-stopped
|
|
15
13
|
healthcheck:
|
|
16
14
|
test: "curl 'http://localhost:1234/'"
|
|
17
15
|
interval: "30s"
|
|
18
16
|
timeout: "3s"
|
|
19
17
|
start_period: "5s"
|
|
20
18
|
retries: 3
|
|
19
|
+
networks:
|
|
20
|
+
- proxy_network
|
|
21
|
+
|
|
21
22
|
console:
|
|
22
23
|
build: .
|
|
23
24
|
command: bundle exec syntropy console
|
|
@@ -31,6 +32,7 @@ services:
|
|
|
31
32
|
- .:/syntropy
|
|
32
33
|
stop_signal: SIGINT
|
|
33
34
|
restart: never
|
|
35
|
+
|
|
34
36
|
test:
|
|
35
37
|
build: .
|
|
36
38
|
command: bundle exec syntropy test -w
|
|
@@ -44,3 +46,26 @@ services:
|
|
|
44
46
|
- .:/syntropy
|
|
45
47
|
stop_signal: SIGINT
|
|
46
48
|
restart: never
|
|
49
|
+
|
|
50
|
+
proxy:
|
|
51
|
+
depends_on:
|
|
52
|
+
- app_server
|
|
53
|
+
image: caddy:2-alpine
|
|
54
|
+
build:
|
|
55
|
+
context: ./proxy
|
|
56
|
+
dockerfile: Dockerfile
|
|
57
|
+
restart: unless-stopped
|
|
58
|
+
ports:
|
|
59
|
+
- "80:80"
|
|
60
|
+
- "443:443"
|
|
61
|
+
- "443:443/udp"
|
|
62
|
+
volumes:
|
|
63
|
+
- ./config/Caddyfile:/etc/caddy/Caddyfile
|
|
64
|
+
- ./storage/caddy/data:/data
|
|
65
|
+
- ./storage/caddy/config:/config
|
|
66
|
+
networks:
|
|
67
|
+
- proxy_network
|
|
68
|
+
|
|
69
|
+
networks:
|
|
70
|
+
proxy_network:
|
|
71
|
+
name: proxy_network
|
data/cmd/new.rb
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
|
|
6
|
+
opts = {}
|
|
7
|
+
|
|
6
8
|
parser = OptionParser.new do |o|
|
|
7
9
|
o.banner = 'Usage: syntropy new NAME [options]'
|
|
8
10
|
|
|
11
|
+
o.on('-y', '--yes', 'Confirm all overwrites') do
|
|
12
|
+
opts[:yes] = true
|
|
13
|
+
end
|
|
14
|
+
|
|
9
15
|
o.on('-h', '--help', 'Show this help message') do
|
|
10
16
|
puts o
|
|
11
17
|
exit
|
|
@@ -37,7 +43,7 @@ template_path = File.join(__dir__, 'new/template')
|
|
|
37
43
|
|
|
38
44
|
begin
|
|
39
45
|
`mkdir -p "#{path}"`
|
|
40
|
-
|
|
46
|
+
system("cp -rv#{opts[:yes] ? '' : 'i'} #{template_path}/* \"#{path}/\"")
|
|
41
47
|
puts "Your app is ready in #{path}"
|
|
42
48
|
rescue => e
|
|
43
49
|
p e
|
data/cmd/serve.rb
CHANGED
|
@@ -94,7 +94,9 @@ env[:banner] = false
|
|
|
94
94
|
env[:machine] = Syntropy.machine = UM.new
|
|
95
95
|
env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
|
|
96
96
|
|
|
97
|
-
require '
|
|
97
|
+
require 'uringmachine/fiber_scheduler'
|
|
98
|
+
Fiber.set_scheduler(UM::FiberScheduler.new(env[:machine]))
|
|
99
|
+
|
|
98
100
|
require 'syntropy/dev_mode' if Syntropy.dev_mode
|
|
99
101
|
|
|
100
102
|
app = Syntropy::App.load(env)
|
data/cmd/version.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'syntropy/version'
|
|
4
|
+
require 'uringmachine/version'
|
|
5
|
+
require_relative './_banner'
|
|
6
|
+
|
|
7
|
+
VERSION = <<~MSG
|
|
8
|
+
Syntropy version #{Syntropy::VERSION}
|
|
9
|
+
UringMachine version #{UringMachine::VERSION}
|
|
10
|
+
Ruby version #{RUBY_VERSION}
|
|
11
|
+
MSG
|
|
12
|
+
|
|
13
|
+
$stdout << SYNTROPY_BANNER
|
|
14
|
+
$stdout << VERSION
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/lib/syntropy/app.rb
CHANGED
|
@@ -9,6 +9,7 @@ require 'syntropy/errors'
|
|
|
9
9
|
require 'syntropy/module_loader'
|
|
10
10
|
require 'syntropy/routing_tree'
|
|
11
11
|
require 'syntropy/mime_types'
|
|
12
|
+
require 'syntropy/controller_extensions'
|
|
12
13
|
|
|
13
14
|
module Syntropy
|
|
14
15
|
# The App implements a Syntropy application. It is responsible for handling
|
|
@@ -34,7 +35,7 @@ module Syntropy
|
|
|
34
35
|
fn = File.join(env[:app_root], '_site.rb')
|
|
35
36
|
return nil if !File.file?(fn)
|
|
36
37
|
|
|
37
|
-
loader = Syntropy::ModuleLoader.new(env)
|
|
38
|
+
loader = Syntropy::ModuleLoader.new(env, extensions: ControllerExtensions)
|
|
38
39
|
loader.load('_site')
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -61,7 +62,10 @@ module Syntropy
|
|
|
61
62
|
@env = env
|
|
62
63
|
@logger = env[:logger]
|
|
63
64
|
|
|
64
|
-
@module_loader = Syntropy::ModuleLoader.new(
|
|
65
|
+
@module_loader = Syntropy::ModuleLoader.new(
|
|
66
|
+
env.merge(app: self),
|
|
67
|
+
extensions: ControllerExtensions
|
|
68
|
+
)
|
|
65
69
|
setup_routing_tree
|
|
66
70
|
start
|
|
67
71
|
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Syntropy
|
|
6
|
+
# Utilities for use in modules
|
|
7
|
+
module ControllerExtensions
|
|
8
|
+
# Returns a unique temporary path
|
|
9
|
+
#
|
|
10
|
+
# @param prefix [String] temp file prefix
|
|
11
|
+
# @return [String]
|
|
12
|
+
def tmp_path(prefix = 'syntropy')
|
|
13
|
+
"/tmp/#{prefix}-#{SecureRandom.hex(16)}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns a request handler that routes request according to the host
|
|
17
|
+
# header. Looks for site directories (named by host name) in the app's root
|
|
18
|
+
# directory. A map may be given in order to provide additional hostnames to
|
|
19
|
+
# site directories.
|
|
20
|
+
#
|
|
21
|
+
# @param dir [String, nil] relative directory path for host sites
|
|
22
|
+
# @param map [Hash, nil] hash mapping host names to relative site directory
|
|
23
|
+
# @return [Proc] router proc
|
|
24
|
+
def dispatch_by_host(dir = nil, map = nil)
|
|
25
|
+
raise Syntropy::Error, 'Must provide dir and/or map' if !dir && !map
|
|
26
|
+
|
|
27
|
+
site_map = {}
|
|
28
|
+
setup_directory_sites(dir, site_map) if dir
|
|
29
|
+
setup_mapped_sites(map, site_map) if map
|
|
30
|
+
|
|
31
|
+
->(req) do
|
|
32
|
+
site = site_map[req.host]
|
|
33
|
+
site ? site.call(req) : req.respond(nil, ':status' => HTTP::BAD_REQUEST)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns a request handler that handles requests by calling the appropriate
|
|
38
|
+
# module method (e.g. get, post, etc.)
|
|
39
|
+
#
|
|
40
|
+
# @return [Proc]
|
|
41
|
+
def dispatch_by_http_method
|
|
42
|
+
->(req) do
|
|
43
|
+
route_by_http_method(req)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns a list of parsed markdown pages at the given path.
|
|
48
|
+
#
|
|
49
|
+
# @param env [Hash] app environment hash
|
|
50
|
+
# @param ref [String] directory path
|
|
51
|
+
# @return [Array<Hash>] array of page entries
|
|
52
|
+
def page_list(env, ref)
|
|
53
|
+
full_path = File.join(env[:app_root], ref)
|
|
54
|
+
raise 'Not a directory' if !File.directory?(full_path)
|
|
55
|
+
|
|
56
|
+
Dir[File.join(full_path, '*.md')].sort.map {
|
|
57
|
+
atts, markdown = Syntropy::Markdown.parse(it, env)
|
|
58
|
+
{ atts:, markdown: }
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Instantiates a Syntropy app for the given environment hash.
|
|
63
|
+
#
|
|
64
|
+
# @return [Syntropy::App]
|
|
65
|
+
def app(**)
|
|
66
|
+
Syntropy::App.new(**)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
|
|
70
|
+
|
|
71
|
+
# Creates a builtin applet with the given environment hash. By default the
|
|
72
|
+
# builtin applet is mounted at /.syntropy.
|
|
73
|
+
#
|
|
74
|
+
# @param env [Hash] app environment
|
|
75
|
+
# @param mount_path [String] mount path for the builtin applet
|
|
76
|
+
# @return [Syntropy::App] applet
|
|
77
|
+
def builtin_applet(env, mount_path: '/.syntropy')
|
|
78
|
+
app(
|
|
79
|
+
machine: env[:machine],
|
|
80
|
+
app_root: BUILTIN_APPLET_app_root,
|
|
81
|
+
mount_path: mount_path,
|
|
82
|
+
builtin_applet_path: nil,
|
|
83
|
+
watch_files: nil
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# Finds sites in the root directory for the given environment hash, adds
|
|
90
|
+
# entries to the given site map.
|
|
91
|
+
#
|
|
92
|
+
# @param dir [String] relative or absolute path
|
|
93
|
+
# @param site_map [Hash] site map
|
|
94
|
+
# @return [void]
|
|
95
|
+
def setup_directory_sites(ref, site_map)
|
|
96
|
+
app_root = @app ? @app.app_root : @env[:app_root]
|
|
97
|
+
ref = normalize_import_ref(ref)
|
|
98
|
+
|
|
99
|
+
Dir[File.join(app_root, ref, '*')]
|
|
100
|
+
.select { File.directory?(it) && File.basename(it) !~ /^_/ }
|
|
101
|
+
.each { |entry| site_map[File.basename(entry)] = make_app(entry) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# converts the given map entries by adding entries to the given site map.
|
|
105
|
+
#
|
|
106
|
+
# @param map [Hash] ref map
|
|
107
|
+
# @param site_map [Hash] site map
|
|
108
|
+
# @return [void]
|
|
109
|
+
def setup_mapped_sites(map, site_map)
|
|
110
|
+
app_root = @app ? @app.app_root : @env[:app_root]
|
|
111
|
+
map.each do |name, ref|
|
|
112
|
+
ref = File.join(File.dirname(@ref), ref) if ref !~ /^\//
|
|
113
|
+
site_root = File.join(app_root, ref)
|
|
114
|
+
site_map[name] = make_app(site_root)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Creates an app loaded from the given root directory, with the present
|
|
119
|
+
# mount path.
|
|
120
|
+
def make_app(site_root)
|
|
121
|
+
mount_path = @ref == '/_site' ? '/' : @ref
|
|
122
|
+
env = @env.merge(app_root: site_root, mount_path:)
|
|
123
|
+
Syntropy::App.new(**env)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Handles the given request by calling the module method corresponding to
|
|
127
|
+
# the request's HTTP method. If no method is found, raises a
|
|
128
|
+
# method_not_allowed error.
|
|
129
|
+
def route_by_http_method(req)
|
|
130
|
+
sym = req.method.to_sym
|
|
131
|
+
raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
|
|
132
|
+
|
|
133
|
+
send(sym, req)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -76,6 +76,9 @@ module Syntropy
|
|
|
76
76
|
def http_read_body(headers)
|
|
77
77
|
content_length = headers['content-length']
|
|
78
78
|
if content_length
|
|
79
|
+
content_length = content_length.to_i
|
|
80
|
+
return nil if content_length == 0
|
|
81
|
+
|
|
79
82
|
chunk = read(content_length.to_i)
|
|
80
83
|
return chunk
|
|
81
84
|
end
|
|
@@ -95,6 +98,9 @@ module Syntropy
|
|
|
95
98
|
def http_skip_body(headers)
|
|
96
99
|
content_length = headers['content-length']
|
|
97
100
|
if content_length
|
|
101
|
+
content_length = content_length.to_i
|
|
102
|
+
return if content_length == 0
|
|
103
|
+
|
|
98
104
|
return skip(content_length.to_i)
|
|
99
105
|
end
|
|
100
106
|
|
|
@@ -110,6 +116,9 @@ module Syntropy
|
|
|
110
116
|
def http_read_body_chunk(headers)
|
|
111
117
|
content_length = headers['content-length']
|
|
112
118
|
if content_length
|
|
119
|
+
content_length = content_length.to_i
|
|
120
|
+
return nil if content_length == 0
|
|
121
|
+
|
|
113
122
|
chunk = read(content_length.to_i)
|
|
114
123
|
return chunk
|
|
115
124
|
end
|
data/lib/syntropy/json_api.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'papercraft'
|
|
4
|
+
require 'syntropy/errors'
|
|
4
5
|
|
|
5
6
|
module Syntropy
|
|
6
7
|
# The ModuleLoader class implemenets a module loader. It handles loading of
|
|
@@ -25,12 +26,14 @@ module Syntropy
|
|
|
25
26
|
# Instantiates a module loader
|
|
26
27
|
#
|
|
27
28
|
# @param env [Hash] environment hash
|
|
29
|
+
# @param extensions [Module, Array<Module>] extension module(s)
|
|
28
30
|
# @return [void]
|
|
29
|
-
def initialize(env)
|
|
31
|
+
def initialize(env, extensions: nil)
|
|
30
32
|
@env = env
|
|
31
33
|
@app_root = env[:app_root]
|
|
32
34
|
@modules = {} # maps ref to module entry
|
|
33
35
|
@fn_map = {} # maps filename to ref
|
|
36
|
+
@extensions = extensions
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
# Loads a module (if not already loaded) and returns its export value.
|
|
@@ -131,7 +134,7 @@ module Syntropy
|
|
|
131
134
|
@fn_map[fn] = ref
|
|
132
135
|
code = IO.read(fn)
|
|
133
136
|
env = @env.merge(module_loader: self, ref: clean_ref(ref))
|
|
134
|
-
mod = Syntropy::
|
|
137
|
+
mod = Syntropy::ModuleContext.load(env, code, fn, @extensions)
|
|
135
138
|
add_dependencies(ref, mod.__dependencies__)
|
|
136
139
|
export_value = transform_module_export_value(
|
|
137
140
|
mod.__export_value__, fn, raise_on_missing:
|
|
@@ -150,10 +153,10 @@ module Syntropy
|
|
|
150
153
|
# @param ref [String] input ref
|
|
151
154
|
# @return [String] clean ref
|
|
152
155
|
def clean_ref(ref)
|
|
153
|
-
return '/' if ref =~ /^index
|
|
156
|
+
return '/' if ref =~ /^index[+]?$/
|
|
154
157
|
|
|
155
|
-
clean = ref.gsub(/\/index
|
|
156
|
-
clean == '' ? '/' : clean
|
|
158
|
+
clean = ref.gsub(/\/index[+]?$/, '')
|
|
159
|
+
(clean == '') ? '/' : clean
|
|
157
160
|
end
|
|
158
161
|
|
|
159
162
|
# Transforms the given export value. If the value is nil, an exception is
|
|
@@ -167,17 +170,15 @@ module Syntropy
|
|
|
167
170
|
raise Syntropy::Error, "No export found in #{fn}" if raise_on_missing
|
|
168
171
|
when String
|
|
169
172
|
->(req) { req.respond(export_value) }
|
|
170
|
-
when Class
|
|
171
|
-
export_value.new(@env)
|
|
172
173
|
else
|
|
173
174
|
export_value
|
|
174
175
|
end
|
|
175
176
|
end
|
|
176
177
|
end
|
|
177
178
|
|
|
178
|
-
# The Syntropy::
|
|
179
|
-
# `.rb` source file that implements a route endpoint, a template,
|
|
180
|
-
# methods or any other functionality needed by the web app.
|
|
179
|
+
# The Syntropy::ModuleContext class provides a context for loading a module. A
|
|
180
|
+
# module is a `.rb` source file that implements a route endpoint, a template,
|
|
181
|
+
# utility methods or any other functionality needed by the web app.
|
|
181
182
|
#
|
|
182
183
|
# The following instance variables are available to modules:
|
|
183
184
|
#
|
|
@@ -191,35 +192,45 @@ module Syntropy
|
|
|
191
192
|
# In addition, the module code also has access to the `MODULE` constant which
|
|
192
193
|
# is set to `self`, and may be used to refer to various methods defined in the
|
|
193
194
|
# module.
|
|
194
|
-
class
|
|
195
|
+
class ModuleContext
|
|
195
196
|
# Loads a module, returning the module instance
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
# @param env [Hash] app environment
|
|
198
|
+
# @param code [String] module source code
|
|
199
|
+
# @param fn [String] module file name
|
|
200
|
+
# @param extensions [Module, Array<Module>] extension module(s)
|
|
201
|
+
# @return [Syntropy::ModuleContext] created module context
|
|
202
|
+
def self.load(env, code, fn, extensions)
|
|
203
|
+
mod = new(env)
|
|
204
|
+
apply_extensions(mod, extensions)
|
|
205
|
+
mod.instance_eval(code, fn)
|
|
199
206
|
env[:logger]&.info(message: "Loaded module at #{fn}")
|
|
200
|
-
|
|
201
|
-
rescue SyntaxError => e
|
|
207
|
+
mod
|
|
208
|
+
rescue StandardError, SyntaxError => e
|
|
202
209
|
env[:logger]&.error(message: "Error while loading module at #{fn}", error: e)
|
|
203
|
-
|
|
210
|
+
e.is_a?(SyntaxError) ? handle_syntax_error(env, e) : (raise e)
|
|
211
|
+
end
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
# Applies the given extension(s) to the given module context.
|
|
214
|
+
#
|
|
215
|
+
# @param mod [Syntropy::ModuleContext] module context
|
|
216
|
+
# @param extensions [Module, Array<Module>] extension module(s)
|
|
217
|
+
def self.apply_extensions(mod, extensions)
|
|
218
|
+
case extensions
|
|
219
|
+
when Array
|
|
220
|
+
extensions.each { mod.extend(it) }
|
|
221
|
+
when Module
|
|
222
|
+
mod.extend(extensions)
|
|
223
|
+
when nil # return
|
|
210
224
|
else
|
|
211
|
-
raise
|
|
225
|
+
raise Syntropy::Error, "Invalid module extensions: #{extensions.inspect}"
|
|
212
226
|
end
|
|
213
|
-
rescue => e
|
|
214
|
-
env[:logger]&.error(message: "Error while loading module at #{fn}", error: e)
|
|
215
|
-
raise e
|
|
216
227
|
end
|
|
217
228
|
|
|
218
229
|
# Initializes a module with the given environment hash.
|
|
219
230
|
#
|
|
220
231
|
# @param env [Hash] environment hash
|
|
221
232
|
# @return [void]
|
|
222
|
-
def initialize(
|
|
233
|
+
def initialize(env)
|
|
223
234
|
@env = env
|
|
224
235
|
@machine = env[:machine]
|
|
225
236
|
@module_loader = env[:module_loader]
|
|
@@ -331,22 +342,15 @@ module Syntropy
|
|
|
331
342
|
Syntropy::App.new(**env)
|
|
332
343
|
end
|
|
333
344
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def http_methods
|
|
339
|
-
->(req) { route_by_http_method(req) }
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
# Handles the given request by calling the module method corresponding to
|
|
343
|
-
# the request's HTTP method. If no method is found, raises a
|
|
344
|
-
# method_not_allowed error.
|
|
345
|
-
def route_by_http_method(req)
|
|
346
|
-
sym = req.method.to_sym
|
|
347
|
-
raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
|
|
345
|
+
def handle_syntax_error(env, e)
|
|
346
|
+
$stderr.puts("\n#{e.message}") if !Syntropy.test_mode
|
|
347
|
+
m = e.message.match(/^(.+): syntax/)
|
|
348
|
+
raise e if !m
|
|
348
349
|
|
|
349
|
-
|
|
350
|
+
location = m[1]
|
|
351
|
+
e2 = SyntaxError.new("Syntax errors found in module #{env[:ref]}")
|
|
352
|
+
e2.set_backtrace([location] + e.backtrace)
|
|
353
|
+
raise e2
|
|
350
354
|
end
|
|
351
355
|
end
|
|
352
356
|
end
|