syntropy 0.10.1 → 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.
@@ -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
@@ -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