syntropy 0.4 → 0.6
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 +10 -0
- data/TODO.md +48 -13
- data/bin/syntropy +1 -1
- data/lib/syntropy/app.rb +40 -120
- data/lib/syntropy/file_watch.rb +5 -4
- data/lib/syntropy/module.rb +1 -1
- data/lib/syntropy/router.rb +240 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +2 -1
- data/test/app/_hook.rb +5 -0
- data/test/app/about/_error.rb +6 -0
- data/test/app/about/raise.rb +3 -0
- data/test/app_custom/_site.rb +3 -0
- data/test/test_app.rb +50 -43
- data/test/test_file_watch.rb +4 -4
- data/test/test_router.rb +90 -0
- metadata +7 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 553553edc3b0c81902bc3a77470bc6e9789fcd757066dcd5b4e790d38f7a28bf
|
4
|
+
data.tar.gz: fdb2cec5f581ba49d5156cce188f31a4811d29767c71833bfcfa37899ee0efc5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7340ecb01725dcc15a78a66fef5f87a113c2e9759a2305753241b0c1cf738f45b3fd6fea2dcf7bca55196cc3fc309db2ade3e7bb12292fc5aa3abe63362403f9
|
7
|
+
data.tar.gz: 48427e653e2c99022177e5475ef53b66c57143288f43e4997c40f1ba464728b310c5e0df343e7f03e3ed79f9fa8637e305b6a22296b19fb15068b7228cf0531f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## 0.6 2025-07-05
|
2
|
+
|
3
|
+
- Add support for middleware and error handlers
|
4
|
+
|
5
|
+
## 0.5 2025-07-05
|
6
|
+
|
7
|
+
- Refactor App class to use Router
|
8
|
+
- Refactor routing functionality into separate Router class
|
9
|
+
- Add support for _site.rb file
|
10
|
+
|
1
11
|
## 0.4 2025-07-03
|
2
12
|
|
3
13
|
- Improve errors API
|
data/TODO.md
CHANGED
@@ -1,23 +1,58 @@
|
|
1
|
-
-
|
1
|
+
- Some standard middleware:
|
2
|
+
|
3
|
+
- request rewriter
|
4
|
+
- logger
|
5
|
+
- auth
|
6
|
+
- selector + terminator
|
2
7
|
|
3
8
|
```Ruby
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
+
# For the chainable DSL shown below, we need to create a custom class:
|
10
|
+
class Syntropy::Middleware::Selector
|
11
|
+
def initialize(select_proc, terminator_proc = nil)
|
12
|
+
@select_proc = select_proc
|
13
|
+
@terminator_proc = terminator_proc
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_proc
|
17
|
+
->(req, proc) {
|
18
|
+
@select_proc.(req) ? @terminator_proc.(req) : proc(req)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def terminate(&proc)
|
23
|
+
@terminator_proc = proc
|
24
|
+
end
|
9
25
|
end
|
10
26
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
27
|
+
def Syntropy
|
28
|
+
```
|
29
|
+
|
30
|
+
```Ruby
|
31
|
+
# a _site.rb file can be used to wrap a whole app
|
32
|
+
# site/_site.rb
|
33
|
+
|
34
|
+
# this means we route according to the host header, with each
|
35
|
+
export Syntropy.route_by_host
|
36
|
+
|
37
|
+
# we can also rewrite requests:
|
38
|
+
rewriter = Syntropy
|
39
|
+
.select { it.host =~ /^tolkora\.(org|com)$/ }
|
40
|
+
.terminate { it.redirect_permanent('https://tolkora.net') }
|
41
|
+
|
42
|
+
# This is actuall a pretty interesting DSL design:
|
43
|
+
# a chain of operations that compose functions. So, we can select a
|
44
|
+
export rewriter.wrap(default_app)
|
45
|
+
|
46
|
+
# composing
|
47
|
+
export rewriter.wrap(Syntropy.some_custom_app.wrap(app))
|
48
|
+
|
49
|
+
# or maybe
|
50
|
+
export rewriter << some_other_middleware << app
|
18
51
|
```
|
19
52
|
|
20
53
|
|
54
|
+
|
55
|
+
|
21
56
|
- CLI tool for setting up a site repo:
|
22
57
|
|
23
58
|
```bash
|
data/bin/syntropy
CHANGED
@@ -61,5 +61,5 @@ end
|
|
61
61
|
opts[:machine] = Syntropy.machine = UM.new
|
62
62
|
opts[:logger] = opts[:logger] && TP2::Logger.new(opts[:machine], **opts)
|
63
63
|
|
64
|
-
app = Syntropy::App.
|
64
|
+
app = Syntropy::App.load(opts)
|
65
65
|
TP2.run(opts) { app.call(it) }
|
data/lib/syntropy/app.rb
CHANGED
@@ -12,38 +12,46 @@ require 'syntropy/module'
|
|
12
12
|
|
13
13
|
module Syntropy
|
14
14
|
class App
|
15
|
-
|
15
|
+
class << self
|
16
|
+
def load(opts)
|
17
|
+
site_file_app(opts) || default_app(opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def site_file_app(opts)
|
23
|
+
site_fn = File.join(opts[:location], '_site.rb')
|
24
|
+
return nil if !File.file?(site_fn)
|
25
|
+
|
26
|
+
loader = Syntropy::ModuleLoader.new(opts[:location], opts)
|
27
|
+
loader.load('_site')
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_app(opts)
|
31
|
+
new(opts[:machine], opts[:location], opts[:mount_path] || '/', opts)
|
32
|
+
end
|
33
|
+
end
|
16
34
|
|
17
35
|
def initialize(machine, src_path, mount_path, opts = {})
|
18
36
|
@machine = machine
|
19
37
|
@src_path = File.expand_path(src_path)
|
20
38
|
@mount_path = mount_path
|
21
|
-
@route_cache = {}
|
22
39
|
@opts = opts
|
23
40
|
|
24
|
-
@
|
41
|
+
@module_loader = Syntropy::ModuleLoader.new(@src_path, @opts)
|
42
|
+
@router = Syntropy::Router.new(@opts, @module_loader)
|
43
|
+
|
25
44
|
@machine.spin do
|
26
45
|
# we do startup stuff asynchronously, in order to first let TP2 do its
|
27
46
|
# setup tasks
|
28
47
|
@machine.sleep 0.15
|
29
48
|
@opts[:logger]&.call("Serving from #{File.expand_path(@src_path)}")
|
30
|
-
start_file_watcher if opts[:watch_files]
|
49
|
+
@router.start_file_watcher if opts[:watch_files]
|
31
50
|
end
|
32
|
-
|
33
|
-
@module_loader ||= Syntropy::ModuleLoader.new(@src_path, @opts)
|
34
|
-
end
|
35
|
-
|
36
|
-
def find_route(path, cache: true)
|
37
|
-
cached = @route_cache[path]
|
38
|
-
return cached if cached
|
39
|
-
|
40
|
-
entry = calculate_route(path)
|
41
|
-
@route_cache[path] = entry if entry[:kind] != :not_found && cache
|
42
|
-
entry
|
43
51
|
end
|
44
52
|
|
45
53
|
def call(req)
|
46
|
-
entry =
|
54
|
+
entry = @router[req.path]
|
47
55
|
render_entry(req, entry)
|
48
56
|
rescue Syntropy::Error => e
|
49
57
|
msg = e.message
|
@@ -56,121 +64,33 @@ module Syntropy
|
|
56
64
|
|
57
65
|
private
|
58
66
|
|
59
|
-
def
|
60
|
-
|
61
|
-
|
62
|
-
period = wf.is_a?(Numeric) ? wf : 0.1
|
63
|
-
@machine.spin do
|
64
|
-
Syntropy.file_watch(@machine, @src_path, period: period) do
|
65
|
-
@opts[:logger]&.call("Detected changed file: #{it}")
|
66
|
-
invalidate_cache(it)
|
67
|
-
rescue Exception => e
|
68
|
-
p e
|
69
|
-
p e.backtrace
|
70
|
-
exit!
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def invalidate_cache(fn)
|
76
|
-
@module_loader.unload(fn)
|
77
|
-
|
78
|
-
invalidated_keys = []
|
79
|
-
@route_cache.each do |k, v|
|
80
|
-
@opts[:logger]&.call("Invalidate cache for #{k}", nil)
|
81
|
-
invalidated_keys << k if v[:fn] == fn
|
82
|
-
end
|
83
|
-
|
84
|
-
invalidated_keys.each { @route_cache.delete(it) }
|
85
|
-
end
|
86
|
-
|
87
|
-
def calculate_relative_path_re(mount_path)
|
88
|
-
mount_path = '' if mount_path == '/'
|
89
|
-
%r{^#{mount_path}(?:/(.*))?$}
|
90
|
-
end
|
91
|
-
|
92
|
-
FILE_KINDS = {
|
93
|
-
'.rb' => :module,
|
94
|
-
'.md' => :markdown
|
95
|
-
}
|
96
|
-
NOT_FOUND = { kind: :not_found }
|
97
|
-
|
98
|
-
# We don't allow access to path with /.., or entries that start with _
|
99
|
-
FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
|
100
|
-
|
101
|
-
def calculate_route(path)
|
102
|
-
return NOT_FOUND if path =~ FORBIDDEN_RE
|
103
|
-
|
104
|
-
m = path.match(@relative_path_re)
|
105
|
-
return NOT_FOUND if !m
|
106
|
-
|
107
|
-
relative_path = m[1] || ''
|
108
|
-
fs_path = File.join(@src_path, relative_path)
|
109
|
-
|
110
|
-
return file_entry(fs_path) if File.file?(fs_path)
|
111
|
-
return find_index_entry(fs_path) if File.directory?(fs_path)
|
112
|
-
|
113
|
-
entry = find_file_entry_with_extension(fs_path)
|
114
|
-
return entry if entry[:kind] != :not_found
|
115
|
-
|
116
|
-
find_up_tree_module(path)
|
117
|
-
end
|
118
|
-
|
119
|
-
def file_entry(fn)
|
120
|
-
{ fn: File.expand_path(fn), kind: FILE_KINDS[File.extname(fn)] || :static }
|
121
|
-
end
|
122
|
-
|
123
|
-
def find_index_entry(dir)
|
124
|
-
find_file_entry_with_extension(File.join(dir, 'index'))
|
125
|
-
end
|
126
|
-
|
127
|
-
def find_file_entry_with_extension(path)
|
128
|
-
fn = "#{path}.html"
|
129
|
-
return file_entry(fn) if File.file?(fn)
|
130
|
-
|
131
|
-
fn = "#{path}.md"
|
132
|
-
return file_entry(fn) if File.file?(fn)
|
133
|
-
|
134
|
-
fn = "#{path}.rb"
|
135
|
-
return file_entry(fn) if File.file?(fn)
|
136
|
-
|
137
|
-
fn = "#{path}+.rb"
|
138
|
-
return file_entry(fn) if File.file?(fn)
|
139
|
-
|
140
|
-
NOT_FOUND
|
141
|
-
end
|
142
|
-
|
143
|
-
def find_up_tree_module(path)
|
144
|
-
parent = parent_path(path)
|
145
|
-
return NOT_FOUND if !parent
|
67
|
+
def render_entry(req, entry)
|
68
|
+
kind = entry[:kind]
|
69
|
+
return respond_not_found(req) if kind == :not_found
|
146
70
|
|
147
|
-
entry
|
148
|
-
entry[:
|
71
|
+
entry[:proc] ||= calculate_route_proc(entry)
|
72
|
+
entry[:proc].(req)
|
149
73
|
end
|
150
74
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
m = path.match(UP_TREE_PATH_RE)
|
155
|
-
m && m[1]
|
75
|
+
def calculate_route_proc(entry)
|
76
|
+
render_proc = route_render_proc(entry)
|
77
|
+
@router.calc_route_proc_with_hooks(entry, render_proc)
|
156
78
|
end
|
157
79
|
|
158
|
-
def
|
80
|
+
def route_render_proc(entry)
|
159
81
|
case entry[:kind]
|
160
|
-
when :not_found
|
161
|
-
respond_not_found(req, entry)
|
162
82
|
when :static
|
163
|
-
respond_static(req, entry)
|
83
|
+
->(req) { respond_static(req, entry) }
|
164
84
|
when :markdown
|
165
|
-
respond_markdown(req, entry)
|
85
|
+
->(req) { respond_markdown(req, entry) }
|
166
86
|
when :module
|
167
|
-
|
87
|
+
load_module(entry)
|
168
88
|
else
|
169
89
|
raise 'Invalid entry kind'
|
170
90
|
end
|
171
91
|
end
|
172
92
|
|
173
|
-
def respond_not_found(req
|
93
|
+
def respond_not_found(req)
|
174
94
|
headers = { ':status' => Qeweney::Status::NOT_FOUND }
|
175
95
|
case req.method
|
176
96
|
when 'head'
|
@@ -199,13 +119,13 @@ module Syntropy
|
|
199
119
|
end
|
200
120
|
|
201
121
|
def respond_module(req, entry)
|
202
|
-
entry[:
|
203
|
-
if entry[:
|
122
|
+
entry[:proc] ||= load_module(entry)
|
123
|
+
if entry[:proc] == :invalid
|
204
124
|
req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
205
125
|
return
|
206
126
|
end
|
207
127
|
|
208
|
-
entry[:
|
128
|
+
entry[:proc].call(req)
|
209
129
|
rescue Syntropy::Error => e
|
210
130
|
req.respond(nil, ':status' => e.http_status)
|
211
131
|
rescue StandardError => e
|
data/lib/syntropy/file_watch.rb
CHANGED
@@ -8,15 +8,16 @@ module Syntropy
|
|
8
8
|
|
9
9
|
queue = Thread::Queue.new
|
10
10
|
listener = Listen.to(*roots) do |modified, added, removed|
|
11
|
-
|
12
|
-
|
11
|
+
modified.each { queue.push([:modified, it]) }
|
12
|
+
added.each { queue.push([:added, it]) }
|
13
|
+
removed.each { queue.push([:removed, it]) }
|
13
14
|
end
|
14
15
|
listener.start
|
15
16
|
|
16
17
|
loop do
|
17
18
|
machine.sleep(period) while queue.empty?
|
18
|
-
fn = queue.shift
|
19
|
-
block.call(fn)
|
19
|
+
event, fn = queue.shift
|
20
|
+
block.call(event, fn)
|
20
21
|
end
|
21
22
|
rescue StandardError => e
|
22
23
|
p e
|
data/lib/syntropy/module.rb
CHANGED
@@ -0,0 +1,240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Syntropy
|
4
|
+
class Router
|
5
|
+
def initialize(opts, module_loader = nil)
|
6
|
+
raise 'Invalid location given' if !File.directory?(opts[:location])
|
7
|
+
|
8
|
+
@opts = opts
|
9
|
+
@machine = opts[:machine]
|
10
|
+
@root = File.expand_path(opts[:location])
|
11
|
+
@mount_path = opts[:mount_path] || '/'
|
12
|
+
@rel_path_re ||= /^#{@root}/
|
13
|
+
@module_loader = module_loader
|
14
|
+
|
15
|
+
@cache = {} # maps url path to route entry
|
16
|
+
@routes = {} # maps canonical path to route entry (actual routes)
|
17
|
+
@files = {} # maps filename to entry
|
18
|
+
@deps = {} # maps filenames to array of dependent entries
|
19
|
+
@x = {} # maps directories to hook chains
|
20
|
+
|
21
|
+
scan_routes
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](path)
|
25
|
+
get_route_entry(path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def start_file_watcher
|
29
|
+
@opts[:logger]&.call('Watching for file changes...', nil)
|
30
|
+
@machine.spin { file_watcher_loop }
|
31
|
+
end
|
32
|
+
|
33
|
+
def calc_route_proc_with_hooks(entry, proc)
|
34
|
+
compose_up_tree_hooks(entry[:fn], proc)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
HIDDEN_RE = /^_/
|
40
|
+
|
41
|
+
def scan_routes(dir = nil)
|
42
|
+
dir ||= @root
|
43
|
+
|
44
|
+
Dir[File.join(dir, '*')].each do
|
45
|
+
basename = File.basename(it)
|
46
|
+
next if (basename =~ HIDDEN_RE)
|
47
|
+
|
48
|
+
File.directory?(it) ? scan_routes(it) : add_route(it)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_route(fn)
|
53
|
+
kind = route_kind(fn)
|
54
|
+
rel_path = path_rel(fn)
|
55
|
+
canonical_path = path_canonical(rel_path, kind)
|
56
|
+
entry = { kind:, fn:, canonical_path: }
|
57
|
+
entry[:handle_subtree] = true if (kind == :module) && !!(fn =~ /\+\.rb$/)
|
58
|
+
|
59
|
+
@routes[canonical_path] = entry
|
60
|
+
@files[fn] = entry
|
61
|
+
end
|
62
|
+
|
63
|
+
def route_kind(fn)
|
64
|
+
case File.extname(fn)
|
65
|
+
when '.md'
|
66
|
+
:markdown
|
67
|
+
when '.rb'
|
68
|
+
:module
|
69
|
+
else
|
70
|
+
:static
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def path_rel(path)
|
75
|
+
path.gsub(@rel_path_re, '')
|
76
|
+
end
|
77
|
+
|
78
|
+
def path_abs(path, base)
|
79
|
+
File.join(base, path)
|
80
|
+
end
|
81
|
+
|
82
|
+
PATH_PARENT_RE = /^(.+)?\/([^\/]+)$/
|
83
|
+
|
84
|
+
def path_parent(path)
|
85
|
+
return nil if path == '/'
|
86
|
+
|
87
|
+
path.match(PATH_PARENT_RE)[1] || '/'
|
88
|
+
end
|
89
|
+
|
90
|
+
MD_EXT_RE = /\.md$/
|
91
|
+
RB_EXT_RE = /[+]?\.rb$/
|
92
|
+
INDEX_RE = /^(.*)\/index[+]?\.(?:rb|md|html)$/
|
93
|
+
|
94
|
+
def path_canonical(rel_path, kind)
|
95
|
+
clean = path_clean(rel_path, kind)
|
96
|
+
clean.empty? ? @mount_path : File.join(@mount_path, clean)
|
97
|
+
end
|
98
|
+
|
99
|
+
def path_clean(rel_path, kind)
|
100
|
+
if (m = rel_path.match(INDEX_RE))
|
101
|
+
return m[1]
|
102
|
+
end
|
103
|
+
|
104
|
+
case kind
|
105
|
+
when :static
|
106
|
+
rel_path
|
107
|
+
when :markdown
|
108
|
+
rel_path.gsub(MD_EXT_RE, '')
|
109
|
+
when :module
|
110
|
+
rel_path.gsub(RB_EXT_RE, '')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def get_route_entry(path, use_cache: true)
|
115
|
+
if use_cache
|
116
|
+
cached = @cache[path]
|
117
|
+
return cached if cached
|
118
|
+
end
|
119
|
+
|
120
|
+
entry = find_route_entry(path)
|
121
|
+
set_cache(path, entry) if use_cache && entry[:kind] != :not_found
|
122
|
+
entry
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_cache(path, entry)
|
126
|
+
@cache[path] = entry
|
127
|
+
(entry[:cache_keys] ||= {})[path] = true
|
128
|
+
end
|
129
|
+
|
130
|
+
# We don't allow access to path with /.., or entries that start with _
|
131
|
+
FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
|
132
|
+
NOT_FOUND = { kind: :not_found }.freeze
|
133
|
+
|
134
|
+
def find_route_entry(path)
|
135
|
+
return NOT_FOUND if path =~ FORBIDDEN_RE
|
136
|
+
|
137
|
+
@routes[path] || find_index_route(path) || find_up_tree_module(path) || NOT_FOUND
|
138
|
+
end
|
139
|
+
|
140
|
+
INDEX_OPT_EXT_RE = /^(.*)\/index(?:\.(?:rb|md|html))?$/
|
141
|
+
|
142
|
+
def find_index_route(path)
|
143
|
+
m = path.match(INDEX_OPT_EXT_RE)
|
144
|
+
return nil if !m
|
145
|
+
|
146
|
+
@routes[m[1]]
|
147
|
+
end
|
148
|
+
|
149
|
+
def find_up_tree_module(path)
|
150
|
+
parent_path = path_parent(path)
|
151
|
+
return nil if !parent_path
|
152
|
+
|
153
|
+
entry = @routes[parent_path]
|
154
|
+
return entry if entry && entry[:handle_subtree]
|
155
|
+
|
156
|
+
find_up_tree_module(parent_path)
|
157
|
+
end
|
158
|
+
|
159
|
+
def file_watcher_loop
|
160
|
+
wf = @opts[:watch_files]
|
161
|
+
period = wf.is_a?(Numeric) ? wf : 0.1
|
162
|
+
Syntropy.file_watch(@machine, @root, period: period) do |event, fn|
|
163
|
+
handle_changed_file(event, fn)
|
164
|
+
rescue Exception => e
|
165
|
+
p e
|
166
|
+
p e.backtrace
|
167
|
+
exit!
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def handle_changed_file(event, fn)
|
172
|
+
@opts[:logger]&.call("Detected changed file: #{event} #{fn}")
|
173
|
+
@module_loader&.invalidate(fn)
|
174
|
+
case event
|
175
|
+
when :added
|
176
|
+
handle_added_file(fn)
|
177
|
+
when :removed
|
178
|
+
handle_removed_file(fn)
|
179
|
+
when :modified
|
180
|
+
handle_modified_file(fn)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def handle_added_file(fn)
|
185
|
+
add_route(fn)
|
186
|
+
@cache.clear # TODO: remove only relevant cache entries
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_removed_file(fn)
|
190
|
+
entry = @files[fn]
|
191
|
+
if entry
|
192
|
+
remove_entry_cache_keys(entry)
|
193
|
+
@routes.delete(entry[:canonical_path])
|
194
|
+
@files.delete(fn)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def handle_modified_file(fn)
|
199
|
+
entry = @files[fn]
|
200
|
+
if entry && entry[:kind] == :module
|
201
|
+
# invalidate the entry proc, so it will be recalculated
|
202
|
+
entry[:proc] = nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def remove_entry_cache_keys(entry)
|
207
|
+
entry[:cache_keys]&.each_key { @cache.delete(it) }.clear
|
208
|
+
end
|
209
|
+
|
210
|
+
def compose_up_tree_hooks(path, proc)
|
211
|
+
parent = File.dirname(path)
|
212
|
+
proc = hook_wrap_if_exists(File.join(parent, '_hook.rb'), proc)
|
213
|
+
proc = error_handler_wrap_if_exists(File.join(parent, '_error.rb'), proc)
|
214
|
+
return proc if parent == @root
|
215
|
+
|
216
|
+
compose_up_tree_hooks(parent, proc)
|
217
|
+
end
|
218
|
+
|
219
|
+
def hook_wrap_if_exists(hook_fn, proc)
|
220
|
+
return proc if !File.file?(hook_fn)
|
221
|
+
|
222
|
+
ref = path_rel(hook_fn).gsub(/\.rb$/, '')
|
223
|
+
hook_proc = @module_loader.load(ref)
|
224
|
+
->(req) { hook_proc.(req, proc) }
|
225
|
+
end
|
226
|
+
|
227
|
+
def error_handler_wrap_if_exists(error_handler_fn, proc)
|
228
|
+
return proc if !File.file?(error_handler_fn)
|
229
|
+
|
230
|
+
ref = path_rel(error_handler_fn).gsub(/\.rb$/, '')
|
231
|
+
error_proc = @module_loader.load(ref)
|
232
|
+
|
233
|
+
proc do |req|
|
234
|
+
proc.(req)
|
235
|
+
rescue StandardError => e
|
236
|
+
error_proc.(req, e)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
@@ -9,11 +9,12 @@ require 'syntropy/connection_pool'
|
|
9
9
|
require 'syntropy/module'
|
10
10
|
require 'syntropy/rpc_api'
|
11
11
|
require 'syntropy/side_run'
|
12
|
+
require 'syntropy/router'
|
12
13
|
require 'syntropy/app'
|
13
14
|
require 'syntropy/request_extensions'
|
14
15
|
|
15
16
|
module Syntropy
|
16
|
-
|
17
|
+
Status = Qeweney::Status
|
17
18
|
|
18
19
|
class << self
|
19
20
|
attr_accessor :machine
|
data/test/app/_hook.rb
ADDED
data/test/test_app.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'helper'
|
4
4
|
|
5
|
-
class
|
5
|
+
class AppTest < Minitest::Test
|
6
6
|
Status = Qeweney::Status
|
7
7
|
|
8
8
|
APP_ROOT = File.join(__dir__, 'app')
|
@@ -13,48 +13,12 @@ class AppRoutingTest < Minitest::Test
|
|
13
13
|
@tmp_path = '/test/tmp'
|
14
14
|
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
15
15
|
|
16
|
-
@app = Syntropy::App.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
def test_find_route
|
24
|
-
entry = @app.find_route('/')
|
25
|
-
assert_equal :not_found, entry[:kind]
|
26
|
-
|
27
|
-
entry = @app.find_route('/test')
|
28
|
-
assert_equal :static, entry[:kind]
|
29
|
-
assert_equal full_path('index.html'), entry[:fn]
|
30
|
-
|
31
|
-
entry = @app.find_route('/test/about')
|
32
|
-
assert_equal :module, entry[:kind]
|
33
|
-
assert_equal full_path('about/index.rb'), entry[:fn]
|
34
|
-
|
35
|
-
entry = @app.find_route('/test/../test_app.rb')
|
36
|
-
assert_equal :not_found, entry[:kind]
|
37
|
-
|
38
|
-
entry = @app.find_route('/test/_layout/default')
|
39
|
-
assert_equal :not_found, entry[:kind]
|
40
|
-
|
41
|
-
entry = @app.find_route('/test/api')
|
42
|
-
assert_equal :module, entry[:kind]
|
43
|
-
assert_equal full_path('api+.rb'), entry[:fn]
|
44
|
-
|
45
|
-
entry = @app.find_route('/test/api/foo/bar')
|
46
|
-
assert_equal :module, entry[:kind]
|
47
|
-
assert_equal full_path('api+.rb'), entry[:fn]
|
48
|
-
|
49
|
-
entry = @app.find_route('/test/api/foo/../bar')
|
50
|
-
assert_equal :not_found, entry[:kind]
|
51
|
-
|
52
|
-
entry = @app.find_route('/test/api_1')
|
53
|
-
assert_equal :not_found, entry[:kind]
|
54
|
-
|
55
|
-
entry = @app.find_route('/test/about/foo')
|
56
|
-
assert_equal :markdown, entry[:kind]
|
57
|
-
assert_equal full_path('about/foo.md'), entry[:fn]
|
16
|
+
@app = Syntropy::App.load(
|
17
|
+
machine: @machine,
|
18
|
+
location: APP_ROOT,
|
19
|
+
mount_path: '/test',
|
20
|
+
watch_files: 0.05
|
21
|
+
)
|
58
22
|
end
|
59
23
|
|
60
24
|
def make_request(*, **)
|
@@ -131,6 +95,10 @@ class AppRoutingTest < Minitest::Test
|
|
131
95
|
assert_equal({ status: 'Error', message: 'Teapot' }, req.response_json)
|
132
96
|
assert_equal Status::TEAPOT, req.response_status
|
133
97
|
|
98
|
+
req = make_request(':method' => 'POST', ':path' => '/test/api/foo/bar?q=incr')
|
99
|
+
assert_equal({ status: 'Error', message: 'Teapot' }, req.response_json)
|
100
|
+
assert_equal Status::TEAPOT, req.response_status
|
101
|
+
|
134
102
|
req = make_request(':method' => 'GET', ':path' => '/test/bar')
|
135
103
|
assert_equal 'foobar', req.response_body
|
136
104
|
assert_equal Status::OK, req.response_status
|
@@ -172,4 +140,43 @@ class AppRoutingTest < Minitest::Test
|
|
172
140
|
ensure
|
173
141
|
IO.write(@tmp_fn, orig_body) if orig_body
|
174
142
|
end
|
143
|
+
|
144
|
+
def test_middleware
|
145
|
+
req = make_request(':method' => 'HEAD', ':path' => '/test?foo=42')
|
146
|
+
assert_equal Status::OK, req.response_status
|
147
|
+
assert_nil req.response_body
|
148
|
+
assert_equal '42', req.ctx[:foo]
|
149
|
+
|
150
|
+
req = make_request(':method' => 'HEAD', ':path' => '/test/about/raise?foo=43')
|
151
|
+
assert_equal Status::INTERNAL_SERVER_ERROR, req.response_status
|
152
|
+
assert_equal '<h1>Raised error</h1>', req.response_body
|
153
|
+
assert_equal '43', req.ctx[:foo]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class CustomAppTest < Minitest::Test
|
158
|
+
Status = Qeweney::Status
|
159
|
+
|
160
|
+
APP_ROOT = File.join(__dir__, 'app_custom')
|
161
|
+
|
162
|
+
def setup
|
163
|
+
@machine = UM.new
|
164
|
+
@app = Syntropy::App.load(
|
165
|
+
machine: @machine,
|
166
|
+
location: APP_ROOT,
|
167
|
+
mount_path: '/'
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
171
|
+
def make_request(*, **)
|
172
|
+
req = mock_req(*, **)
|
173
|
+
@app.call(req)
|
174
|
+
req
|
175
|
+
end
|
176
|
+
|
177
|
+
def test_app_with_site_rb_file
|
178
|
+
req = make_request(':method' => 'GET', ':path' => '/foo/bar')
|
179
|
+
assert_nil req.response_body
|
180
|
+
assert_equal Status::TEAPOT, req.response_status
|
181
|
+
end
|
175
182
|
end
|
data/test/test_file_watch.rb
CHANGED
@@ -14,21 +14,21 @@ class FileWatchTest < Minitest::Test
|
|
14
14
|
queue = UM::Queue.new
|
15
15
|
|
16
16
|
f = @machine.spin do
|
17
|
-
Syntropy.file_watch(@machine, @root, period: 0.01) { @machine.push(queue,
|
17
|
+
Syntropy.file_watch(@machine, @root, period: 0.01) { |event, fn| @machine.push(queue, [event, fn]) }
|
18
18
|
end
|
19
19
|
@machine.sleep(0.05)
|
20
20
|
assert_equal 0, queue.count
|
21
21
|
|
22
22
|
fn = File.join(@root, 'foo.bar')
|
23
23
|
IO.write(fn, 'abc')
|
24
|
-
assert_equal fn, @machine.shift(queue)
|
24
|
+
assert_equal [:added, fn], @machine.shift(queue)
|
25
25
|
|
26
26
|
fn = File.join(@root, 'foo.bar')
|
27
27
|
IO.write(fn, 'def')
|
28
|
-
assert_equal fn, @machine.shift(queue)
|
28
|
+
assert_equal [:modified, fn], @machine.shift(queue)
|
29
29
|
|
30
30
|
FileUtils.rm(fn)
|
31
|
-
assert_equal fn, @machine.shift(queue)
|
31
|
+
assert_equal [:removed, fn], @machine.shift(queue)
|
32
32
|
ensure
|
33
33
|
@machine.schedule(f, UM::Terminate)
|
34
34
|
# @machine.join(f)
|
data/test/test_router.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
class RouterTest < Minitest::Test
|
6
|
+
APP_ROOT = File.join(__dir__, 'app')
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@machine = UM.new
|
10
|
+
|
11
|
+
@tmp_path = '/test/tmp'
|
12
|
+
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
13
|
+
|
14
|
+
@router = Syntropy::Router.new(
|
15
|
+
machine: @machine,
|
16
|
+
location: APP_ROOT,
|
17
|
+
mount_path: '/test',
|
18
|
+
watch_files: 0.05
|
19
|
+
)
|
20
|
+
@router.start_file_watcher
|
21
|
+
end
|
22
|
+
|
23
|
+
def full_path(fn)
|
24
|
+
File.join(APP_ROOT, fn)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_find_route
|
28
|
+
# entry = @router['/']
|
29
|
+
# assert_equal :not_found, entry[:kind]
|
30
|
+
|
31
|
+
entry = @router['/test']
|
32
|
+
assert_equal :static, entry[:kind]
|
33
|
+
assert_equal full_path('index.html'), entry[:fn]
|
34
|
+
|
35
|
+
entry = @router['/test/about']
|
36
|
+
assert_equal :module, entry[:kind]
|
37
|
+
assert_equal full_path('about/index.rb'), entry[:fn]
|
38
|
+
|
39
|
+
entry = @router['/test/../test_app.rb']
|
40
|
+
assert_equal :not_found, entry[:kind]
|
41
|
+
|
42
|
+
entry = @router['/test/_layout/default']
|
43
|
+
assert_equal :not_found, entry[:kind]
|
44
|
+
|
45
|
+
entry = @router['/test/api']
|
46
|
+
assert_equal :module, entry[:kind]
|
47
|
+
assert_equal full_path('api+.rb'), entry[:fn]
|
48
|
+
|
49
|
+
entry = @router['/test/api/foo/bar']
|
50
|
+
assert_equal :module, entry[:kind]
|
51
|
+
assert_equal full_path('api+.rb'), entry[:fn]
|
52
|
+
|
53
|
+
entry = @router['/test/api/foo/../bar']
|
54
|
+
assert_equal :not_found, entry[:kind]
|
55
|
+
|
56
|
+
entry = @router['/test/api_1']
|
57
|
+
assert_equal :not_found, entry[:kind]
|
58
|
+
|
59
|
+
entry = @router['/test/about/foo']
|
60
|
+
assert_equal :markdown, entry[:kind]
|
61
|
+
assert_equal full_path('about/foo.md'), entry[:fn]
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_router_file_watching
|
65
|
+
@machine.sleep 0.2
|
66
|
+
|
67
|
+
entry = @router[@tmp_path]
|
68
|
+
assert_equal :module, entry[:kind]
|
69
|
+
|
70
|
+
# remove file
|
71
|
+
orig_body = IO.read(@tmp_fn)
|
72
|
+
FileUtils.rm(@tmp_fn)
|
73
|
+
@machine.sleep(0.3)
|
74
|
+
|
75
|
+
entry = @router[@tmp_path]
|
76
|
+
assert_equal :not_found, entry[:kind]
|
77
|
+
|
78
|
+
IO.write(@tmp_fn, 'foobar')
|
79
|
+
@machine.sleep(0.3)
|
80
|
+
entry = @router[@tmp_path]
|
81
|
+
assert_equal :module, entry[:kind]
|
82
|
+
|
83
|
+
entry[:proc] = ->(x) { x }
|
84
|
+
IO.write(@tmp_fn, 'barbaz')
|
85
|
+
@machine.sleep(0.3)
|
86
|
+
assert_nil entry[:proc]
|
87
|
+
ensure
|
88
|
+
IO.write(@tmp_fn, orig_body) if orig_body
|
89
|
+
end
|
90
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syntropy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.6'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -197,28 +197,34 @@ files:
|
|
197
197
|
- lib/syntropy/file_watch.rb
|
198
198
|
- lib/syntropy/module.rb
|
199
199
|
- lib/syntropy/request_extensions.rb
|
200
|
+
- lib/syntropy/router.rb
|
200
201
|
- lib/syntropy/rpc_api.rb
|
201
202
|
- lib/syntropy/side_run.rb
|
202
203
|
- lib/syntropy/version.rb
|
203
204
|
- syntropy.gemspec
|
205
|
+
- test/app/_hook.rb
|
204
206
|
- test/app/_layout/default.rb
|
205
207
|
- test/app/_lib/callable.rb
|
206
208
|
- test/app/_lib/klass.rb
|
207
209
|
- test/app/_lib/missing-export.rb
|
210
|
+
- test/app/about/_error.rb
|
208
211
|
- test/app/about/foo.md
|
209
212
|
- test/app/about/index.rb
|
213
|
+
- test/app/about/raise.rb
|
210
214
|
- test/app/api+.rb
|
211
215
|
- test/app/assets/style.css
|
212
216
|
- test/app/bar.rb
|
213
217
|
- test/app/baz.rb
|
214
218
|
- test/app/index.html
|
215
219
|
- test/app/tmp.rb
|
220
|
+
- test/app_custom/_site.rb
|
216
221
|
- test/helper.rb
|
217
222
|
- test/run.rb
|
218
223
|
- test/test_app.rb
|
219
224
|
- test/test_connection_pool.rb
|
220
225
|
- test/test_file_watch.rb
|
221
226
|
- test/test_module.rb
|
227
|
+
- test/test_router.rb
|
222
228
|
- test/test_rpc_api.rb
|
223
229
|
- test/test_side_run.rb
|
224
230
|
- test/test_validation.rb
|