syntropy 0.11 → 0.12
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 +9 -0
- data/Rakefile +1 -1
- data/TODO.md +180 -135
- data/bin/syntropy +8 -3
- data/lib/syntropy/app.rb +227 -111
- data/lib/syntropy/errors.rb +40 -12
- data/lib/syntropy/markdown.rb +4 -2
- data/lib/syntropy/module.rb +9 -10
- data/lib/syntropy/request_extensions.rb +112 -2
- data/lib/syntropy/routing_tree.rb +553 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +1 -1
- data/syntropy.gemspec +1 -1
- data/test/app/params/[foo].rb +3 -0
- data/test/helper.rb +18 -2
- data/test/test_app.rb +17 -25
- data/test/test_errors.rb +38 -0
- data/test/test_request_extensions.rb +163 -0
- data/test/test_routing_tree.rb +427 -0
- metadata +8 -6
- data/lib/syntropy/router.rb +0 -245
- data/test/test_router.rb +0 -116
- data/test/test_validation.rb +0 -35
data/lib/syntropy/router.rb
DELETED
@@ -1,245 +0,0 @@
|
|
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]&.info(
|
30
|
-
message: 'Watching for file changes...'
|
31
|
-
)
|
32
|
-
@machine.spin { file_watcher_loop }
|
33
|
-
end
|
34
|
-
|
35
|
-
def calc_route_proc_with_hooks(entry, proc)
|
36
|
-
compose_up_tree_hooks(entry[:fn], proc)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
HIDDEN_RE = /^_/
|
42
|
-
|
43
|
-
def scan_routes(dir = nil)
|
44
|
-
dir ||= @root
|
45
|
-
|
46
|
-
Dir[File.join(dir, '*')].each do
|
47
|
-
basename = File.basename(it)
|
48
|
-
next if (basename =~ HIDDEN_RE)
|
49
|
-
|
50
|
-
File.directory?(it) ? scan_routes(it) : add_route(it)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def add_route(fn)
|
55
|
-
kind = route_kind(fn)
|
56
|
-
rel_path = path_rel(fn)
|
57
|
-
canonical_path = path_canonical(rel_path, kind)
|
58
|
-
entry = { kind:, fn:, canonical_path: }
|
59
|
-
entry[:handle_subtree] = true if (kind == :module) && !!(fn =~ /\+\.rb$/)
|
60
|
-
|
61
|
-
@routes[canonical_path] = entry
|
62
|
-
@files[fn] = entry
|
63
|
-
end
|
64
|
-
|
65
|
-
def route_kind(fn)
|
66
|
-
case File.extname(fn)
|
67
|
-
when '.md'
|
68
|
-
:markdown
|
69
|
-
when '.rb'
|
70
|
-
:module
|
71
|
-
else
|
72
|
-
:static
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def path_rel(path)
|
77
|
-
path.gsub(@rel_path_re, '')
|
78
|
-
end
|
79
|
-
|
80
|
-
def path_abs(path, base)
|
81
|
-
File.join(base, path)
|
82
|
-
end
|
83
|
-
|
84
|
-
PATH_PARENT_RE = /^(.+)?\/([^\/]+)$/
|
85
|
-
|
86
|
-
def path_parent(path)
|
87
|
-
return nil if path == '/'
|
88
|
-
|
89
|
-
m = path.match(PATH_PARENT_RE)
|
90
|
-
m && (m[1] || '/')
|
91
|
-
end
|
92
|
-
|
93
|
-
MD_EXT_RE = /\.md$/
|
94
|
-
RB_EXT_RE = /[+]?\.rb$/
|
95
|
-
INDEX_RE = /^(.*)\/index[+]?\.(?:rb|md|html)$/
|
96
|
-
|
97
|
-
def path_canonical(rel_path, kind)
|
98
|
-
clean = path_clean(rel_path, kind)
|
99
|
-
clean.empty? ? @mount_path : File.join(@mount_path, clean)
|
100
|
-
end
|
101
|
-
|
102
|
-
def path_clean(rel_path, kind)
|
103
|
-
if (m = rel_path.match(INDEX_RE))
|
104
|
-
return m[1]
|
105
|
-
end
|
106
|
-
|
107
|
-
case kind
|
108
|
-
when :static
|
109
|
-
rel_path
|
110
|
-
when :markdown
|
111
|
-
rel_path.gsub(MD_EXT_RE, '')
|
112
|
-
when :module
|
113
|
-
rel_path.gsub(RB_EXT_RE, '')
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def get_route_entry(path, use_cache: true)
|
118
|
-
if use_cache
|
119
|
-
cached = @cache[path]
|
120
|
-
return cached if cached
|
121
|
-
end
|
122
|
-
|
123
|
-
entry = find_route_entry(path)
|
124
|
-
set_cache(path, entry) if use_cache && entry[:kind] != :not_found
|
125
|
-
entry
|
126
|
-
end
|
127
|
-
|
128
|
-
def set_cache(path, entry)
|
129
|
-
@cache[path] = entry
|
130
|
-
(entry[:cache_keys] ||= {})[path] = true
|
131
|
-
end
|
132
|
-
|
133
|
-
# We don't allow access to path with /.., or entries that start with _
|
134
|
-
FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
|
135
|
-
NOT_FOUND = { kind: :not_found }.freeze
|
136
|
-
|
137
|
-
def find_route_entry(path)
|
138
|
-
return NOT_FOUND if path =~ FORBIDDEN_RE
|
139
|
-
|
140
|
-
@routes[path] || find_index_route(path) || find_up_tree_module(path) || NOT_FOUND
|
141
|
-
end
|
142
|
-
|
143
|
-
INDEX_OPT_EXT_RE = /^(.*)\/index(?:\.(?:rb|md|html))?$/
|
144
|
-
|
145
|
-
def find_index_route(path)
|
146
|
-
m = path.match(INDEX_OPT_EXT_RE)
|
147
|
-
return nil if !m
|
148
|
-
|
149
|
-
@routes[m[1]]
|
150
|
-
end
|
151
|
-
|
152
|
-
def find_up_tree_module(path)
|
153
|
-
parent_path = path_parent(path)
|
154
|
-
return nil if !parent_path
|
155
|
-
|
156
|
-
entry = @routes[parent_path]
|
157
|
-
return entry if entry && entry[:handle_subtree]
|
158
|
-
|
159
|
-
find_up_tree_module(parent_path)
|
160
|
-
end
|
161
|
-
|
162
|
-
def file_watcher_loop
|
163
|
-
wf = @opts[:watch_files]
|
164
|
-
period = wf.is_a?(Numeric) ? wf : 0.1
|
165
|
-
Syntropy.file_watch(@machine, @root, period: period) do |event, fn|
|
166
|
-
handle_changed_file(event, fn)
|
167
|
-
rescue Exception => e
|
168
|
-
p e
|
169
|
-
p e.backtrace
|
170
|
-
exit!
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
def handle_changed_file(event, fn)
|
175
|
-
@opts[:logger]&.info(
|
176
|
-
message: "Detected changed file: #{event} #{fn}"
|
177
|
-
)
|
178
|
-
@module_loader&.invalidate(fn)
|
179
|
-
case event
|
180
|
-
when :added
|
181
|
-
handle_added_file(fn)
|
182
|
-
when :removed
|
183
|
-
handle_removed_file(fn)
|
184
|
-
when :modified
|
185
|
-
handle_modified_file(fn)
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
def handle_added_file(fn)
|
190
|
-
add_route(fn)
|
191
|
-
@cache.clear # TODO: remove only relevant cache entries
|
192
|
-
end
|
193
|
-
|
194
|
-
def handle_removed_file(fn)
|
195
|
-
entry = @files[fn]
|
196
|
-
if entry
|
197
|
-
remove_entry_cache_keys(entry)
|
198
|
-
@routes.delete(entry[:canonical_path])
|
199
|
-
@files.delete(fn)
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
def handle_modified_file(fn)
|
204
|
-
entry = @files[fn]
|
205
|
-
if entry && entry[:kind] == :module
|
206
|
-
# invalidate the entry proc, so it will be recalculated
|
207
|
-
entry[:proc] = nil
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def remove_entry_cache_keys(entry)
|
212
|
-
entry[:cache_keys]&.each_key { @cache.delete(it) }.clear
|
213
|
-
end
|
214
|
-
|
215
|
-
def compose_up_tree_hooks(path, proc)
|
216
|
-
parent = File.dirname(path)
|
217
|
-
proc = hook_wrap_if_exists(File.join(parent, '_hook.rb'), proc)
|
218
|
-
proc = error_handler_wrap_if_exists(File.join(parent, '_error.rb'), proc)
|
219
|
-
return proc if parent == @root
|
220
|
-
|
221
|
-
compose_up_tree_hooks(parent, proc)
|
222
|
-
end
|
223
|
-
|
224
|
-
def hook_wrap_if_exists(hook_fn, proc)
|
225
|
-
return proc if !File.file?(hook_fn)
|
226
|
-
|
227
|
-
ref = path_rel(hook_fn).gsub(/\.rb$/, '')
|
228
|
-
hook_proc = @module_loader.load(ref)
|
229
|
-
->(req) { hook_proc.(req, proc) }
|
230
|
-
end
|
231
|
-
|
232
|
-
def error_handler_wrap_if_exists(error_handler_fn, proc)
|
233
|
-
return proc if !File.file?(error_handler_fn)
|
234
|
-
|
235
|
-
ref = path_rel(error_handler_fn).gsub(/\.rb$/, '')
|
236
|
-
error_proc = @module_loader.load(ref)
|
237
|
-
|
238
|
-
proc do |req|
|
239
|
-
proc.(req)
|
240
|
-
rescue StandardError => e
|
241
|
-
error_proc.(req, e)
|
242
|
-
end
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
data/test/test_router.rb
DELETED
@@ -1,116 +0,0 @@
|
|
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_routing
|
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/foo/../bar']
|
57
|
-
assert_equal :not_found, entry[:kind]
|
58
|
-
|
59
|
-
entry = @router['/test/api_1']
|
60
|
-
assert_equal :not_found, entry[:kind]
|
61
|
-
|
62
|
-
entry = @router['/test/about/foo']
|
63
|
-
assert_equal :markdown, entry[:kind]
|
64
|
-
assert_equal full_path('about/foo.md'), entry[:fn]
|
65
|
-
end
|
66
|
-
|
67
|
-
def test_router_file_watching
|
68
|
-
@machine.sleep 0.2
|
69
|
-
|
70
|
-
entry = @router[@tmp_path]
|
71
|
-
assert_equal :module, entry[:kind]
|
72
|
-
|
73
|
-
# remove file
|
74
|
-
orig_body = IO.read(@tmp_fn)
|
75
|
-
FileUtils.rm(@tmp_fn)
|
76
|
-
@machine.sleep(0.3)
|
77
|
-
|
78
|
-
entry = @router[@tmp_path]
|
79
|
-
assert_equal :not_found, entry[:kind]
|
80
|
-
|
81
|
-
IO.write(@tmp_fn, 'foobar')
|
82
|
-
@machine.sleep(0.3)
|
83
|
-
entry = @router[@tmp_path]
|
84
|
-
assert_equal :module, entry[:kind]
|
85
|
-
|
86
|
-
entry[:proc] = ->(x) { x }
|
87
|
-
IO.write(@tmp_fn, 'barbaz')
|
88
|
-
@machine.sleep(0.3)
|
89
|
-
assert_nil entry[:proc]
|
90
|
-
ensure
|
91
|
-
IO.write(@tmp_fn, orig_body) if orig_body
|
92
|
-
end
|
93
|
-
|
94
|
-
def test_malformed_path_routing
|
95
|
-
entry = @router['//xmlrpc.php?rsd']
|
96
|
-
assert_equal :not_found, entry[:kind]
|
97
|
-
|
98
|
-
entry = @router['/test//xmlrpc.php?rsd']
|
99
|
-
assert_equal :not_found, entry[:kind]
|
100
|
-
|
101
|
-
entry = @router['/test///xmlrpc.php?rsd']
|
102
|
-
assert_equal :not_found, entry[:kind]
|
103
|
-
|
104
|
-
entry = @router['/test///xmlrpc.php?rsd']
|
105
|
-
assert_equal :not_found, entry[:kind]
|
106
|
-
|
107
|
-
entry = @router['/test/./qsdf']
|
108
|
-
assert_equal :not_found, entry[:kind]
|
109
|
-
|
110
|
-
entry = @router['/test/../../lib/syntropy.rb']
|
111
|
-
assert_equal :not_found, entry[:kind]
|
112
|
-
|
113
|
-
entry = @router['/../../lib/syntropy.rb']
|
114
|
-
assert_equal :not_found, entry[:kind]
|
115
|
-
end
|
116
|
-
end
|
data/test/test_validation.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'helper'
|
4
|
-
|
5
|
-
class ValidationTest < Minitest::Test
|
6
|
-
def setup
|
7
|
-
@req = mock_req(':method' => 'GET', ':path' => '/foo?q=foo&x=bar&y=123&z1=t&z2=f')
|
8
|
-
end
|
9
|
-
|
10
|
-
VE = Syntropy::ValidationError
|
11
|
-
|
12
|
-
def test_validate_param
|
13
|
-
assert_nil @req.validate_param(:azerty, nil)
|
14
|
-
assert_equal 'foo', @req.validate_param(:q)
|
15
|
-
assert_equal 'foo', @req.validate_param(:q, String)
|
16
|
-
assert_equal 'foo', @req.validate_param(:q, [String, nil])
|
17
|
-
assert_nil @req.validate_param(:r, [String, nil])
|
18
|
-
|
19
|
-
assert_equal 123, @req.validate_param(:y, Integer)
|
20
|
-
assert_equal 123, @req.validate_param(:y, Integer, 120..125)
|
21
|
-
assert_equal 123.0, @req.validate_param(:y, Float)
|
22
|
-
|
23
|
-
assert_equal true, @req.validate_param(:z1, :bool)
|
24
|
-
assert_equal false, @req.validate_param(:z2, :bool)
|
25
|
-
|
26
|
-
assert_raises(VE) { @req.validate_param(:azerty, String) }
|
27
|
-
assert_raises(VE) { @req.validate_param(:q, Integer) }
|
28
|
-
assert_raises(VE) { @req.validate_param(:q, Float) }
|
29
|
-
assert_raises(VE) { @req.validate_param(:q, nil) }
|
30
|
-
|
31
|
-
assert_raises(VE) { @req.validate_param(:y, Integer, 1..100) }
|
32
|
-
|
33
|
-
assert_raises(VE) { @req.validate_param(:y, :bool) }
|
34
|
-
end
|
35
|
-
end
|