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
@@ -0,0 +1,553 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Syntropy
|
4
|
+
# The RoutingTree class implements a file-based routing tree with support for
|
5
|
+
# static files, markdown files, ruby modules, parametric routes, subtree routes,
|
6
|
+
# nested middleware and error handlers.
|
7
|
+
#
|
8
|
+
# A RoutingTree instance takes the given directory (root_dir) and constructs a
|
9
|
+
# tree of route entries corresponding to the directory's contents. Finally, it
|
10
|
+
# generates an optimized router proc, which is used by the application to return
|
11
|
+
# a route entry for each incoming HTTP request.
|
12
|
+
#
|
13
|
+
# Once initialized, the routing tree is immutable. When running Syntropy in
|
14
|
+
# watch mode, whenever a file or directory is changed, added or deleted, a new
|
15
|
+
# routing tree will be constructed, and the old one will be discarded.
|
16
|
+
#
|
17
|
+
# File-based routing in Syntropy follows some simple rules:
|
18
|
+
#
|
19
|
+
# - Static files (anything other than markdown files or dynamic Ruby modules)
|
20
|
+
# are routed to according to their location in the file tree.
|
21
|
+
# - Index files with `.md` or `.rb` extension handle requests to their
|
22
|
+
# immediate containing directory. For example, `/users/index.rb` will handle
|
23
|
+
# requests to `/users`.
|
24
|
+
# - Index files with a `+` suffix will also handle requests to anywhere in their
|
25
|
+
# subtree. For example, `/users/index+.rb` will also handle requests to
|
26
|
+
# `/users/foo/bar`.
|
27
|
+
# - Other markdown and module files will handle requests to their bare name
|
28
|
+
# (that is, without the extension.) Thus, `/users/foo.rb` will handle requests
|
29
|
+
# to `/users/foo`. A route with a `+` suffix will also handle requests to the
|
30
|
+
# route's subtree. Thus, `/users/foo+.rb` will also handle requests to
|
31
|
+
# `/users/foo/bar`.
|
32
|
+
# - Parametric routes are implemented by enclosing the route name in square
|
33
|
+
# brackets. For example, `/processes/[proc_id]/index.rb` will handle requests
|
34
|
+
# to `/posts/14` etc. Parametric route parts can also be expressed as files,
|
35
|
+
# e.g. `/processes/[id]/sources/[src_id].rb` will handle requests to
|
36
|
+
# `/posts/14/sources/42` etc. The values for placeholders are added to the
|
37
|
+
# incoming request. Here too, a `+` suffix causes the route to also handle
|
38
|
+
# requests to its subtree.
|
39
|
+
# - Directories and files whose names start with an underscore, e.g. `/_foo` or
|
40
|
+
# `/docs/_bar.rb` are skipped and will not be added to the routing tree. This
|
41
|
+
# allows you to prevent access through the HTTP server to protected or
|
42
|
+
# internal modules or files.
|
43
|
+
class RoutingTree
|
44
|
+
attr_reader :root_dir, :mount_path, :static_map, :dynamic_map, :root
|
45
|
+
|
46
|
+
# Initializes a new RoutingTree instance and computes the routing tree
|
47
|
+
#
|
48
|
+
# @param root_dir [String] root directory of file tree
|
49
|
+
# @param mount_path [String] base URL path
|
50
|
+
# @return [void]
|
51
|
+
def initialize(root_dir:, mount_path:, **env)
|
52
|
+
@root_dir = root_dir
|
53
|
+
@mount_path = mount_path
|
54
|
+
@static_map = {}
|
55
|
+
@dynamic_map = {}
|
56
|
+
@env = env
|
57
|
+
@root = compute_tree
|
58
|
+
@static_map.freeze
|
59
|
+
@dynamic_map.freeze
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the generated router proc for the routing tree
|
63
|
+
#
|
64
|
+
# @return [Proc] router proc
|
65
|
+
def router_proc
|
66
|
+
@router_proc ||= compile_router_proc
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the first error handler found for the entry. If no error handler
|
70
|
+
# exists for the entry itself, the search continues up the tree through the
|
71
|
+
# entry's ancestors.
|
72
|
+
#
|
73
|
+
# @param entry [Hash] route entry
|
74
|
+
# @return [String, nil] filename of error handler, or nil if not found
|
75
|
+
def route_error_handler(entry)
|
76
|
+
return entry[:error] if entry[:error]
|
77
|
+
|
78
|
+
entry[:parent] && route_error_handler(entry[:parent])
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a list of all hooks found up the tree from the given entry. Hooks
|
82
|
+
# are returned ordered from the tree root down to the given entry.
|
83
|
+
#
|
84
|
+
# @param entry [Hash] route entry
|
85
|
+
# @return [Array<String>] list of hook entries
|
86
|
+
def route_hooks(entry)
|
87
|
+
hooks = []
|
88
|
+
while entry
|
89
|
+
hooks.unshift(entry[:hook]) if entry[:hook]
|
90
|
+
entry = entry[:parent]
|
91
|
+
end
|
92
|
+
hooks
|
93
|
+
end
|
94
|
+
|
95
|
+
# Computes a "clean" URL path for the given path. Modules and markdown are
|
96
|
+
# stripped of their extensions, and index file paths are also converted to the
|
97
|
+
# containing directory path. For example, the clean URL path for `/foo/bar.rb`
|
98
|
+
# is `/foo/bar`. The Clean URL path for `/bar/baz/index.rb` is `/bar/baz`.
|
99
|
+
#
|
100
|
+
# @param fn [String] file path
|
101
|
+
# @return [String] clean path
|
102
|
+
def compute_clean_url_path(fn)
|
103
|
+
rel_path = fn.sub(@root_dir, '')
|
104
|
+
case rel_path
|
105
|
+
when /^(.*)\/index\.(md|rb|html)$/
|
106
|
+
Regexp.last_match(1).then { it == '' ? '/' : it }
|
107
|
+
when /^(.*)\.(md|rb|html)$/
|
108
|
+
Regexp.last_match(1)
|
109
|
+
else
|
110
|
+
rel_path
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Converts filename to relative path.
|
115
|
+
#
|
116
|
+
# @param fn [String] filename
|
117
|
+
# @return [String] relative path
|
118
|
+
def fn_to_rel_path(fn)
|
119
|
+
fn.sub(/^#{Regexp.escape(@root_dir)}\//, '').sub(/\.[^\.]+$/, '')
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Maps extensions to route kind.
|
125
|
+
FILE_TYPE = {
|
126
|
+
'.rb' => :module,
|
127
|
+
'.md' => :markdown
|
128
|
+
}
|
129
|
+
|
130
|
+
# Computes the routing tree, returning the root entry. Route entries are
|
131
|
+
# represented as hashes with the following keys:
|
132
|
+
#
|
133
|
+
# - `:parent` - reference to the parent entry.
|
134
|
+
# - `:path` - the URL path for the entry.
|
135
|
+
# - `:target` - a hash containing route target information.
|
136
|
+
# - `:param` - the parameter name for parametric routes.
|
137
|
+
# - `:hook` - a reference to the hook module (`_hook.rb`) for the directory,
|
138
|
+
# if exists.
|
139
|
+
# - `:error` - a reference to the error handler module (`_error.rb`) for the
|
140
|
+
# directory, if exists.
|
141
|
+
# - `children` - a hash mapping segment names to the corresponding child
|
142
|
+
# entries.
|
143
|
+
#
|
144
|
+
# Route entries are created for any directory, and for any *dynamic* files
|
145
|
+
# (i.e. markdown or Ruby module files). Files starting with `_` are not
|
146
|
+
# considered as routes and will not be included in the routing tree. Static
|
147
|
+
# files will also not be included in the routing tree, but instead will be
|
148
|
+
# mapped in the static file map (see below).
|
149
|
+
#
|
150
|
+
# The routing tree is complemented with two maps:
|
151
|
+
#
|
152
|
+
# - `static_map` - maps URL paths to the corresponding static route entries.
|
153
|
+
# - `dynamic_map` - maps URL paths to the corresponding dynamic route entries.
|
154
|
+
#
|
155
|
+
# @return [Hash] root entry
|
156
|
+
def compute_tree
|
157
|
+
compute_route_directory(dir: @root_dir, rel_path: '/', parent: nil)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Computes a route entry for a directory.
|
161
|
+
#
|
162
|
+
# @param dir [String] directory path
|
163
|
+
# @param rel_path [String] relative directory path
|
164
|
+
# @param parent [Hash, nil] parent entry
|
165
|
+
def compute_route_directory(dir:, rel_path:, parent: nil)
|
166
|
+
param = (m = File.basename(dir).match(/^\[(.+)\]$/)) ? m[1] : nil
|
167
|
+
entry = {
|
168
|
+
parent: parent,
|
169
|
+
path: rel_path_to_abs_path(rel_path),
|
170
|
+
param: param,
|
171
|
+
hook: find_aux_module_entry(dir, '_hook.rb'),
|
172
|
+
error: find_aux_module_entry(dir, '_error.rb')
|
173
|
+
}
|
174
|
+
entry[:children] = compute_child_routes(
|
175
|
+
dir:, rel_path:, parent: entry
|
176
|
+
)
|
177
|
+
entry
|
178
|
+
end
|
179
|
+
|
180
|
+
# Searches for a file of the given name in the given directory. If found,
|
181
|
+
# returns the file path.
|
182
|
+
#
|
183
|
+
# @param dir [String] directory path
|
184
|
+
# @param name [String] filename
|
185
|
+
# @return [String, nil] file path if found
|
186
|
+
def find_aux_module_entry(dir, name)
|
187
|
+
fn = File.join(dir, name)
|
188
|
+
File.file?(fn) ? ({ kind: :module, fn: fn }) : nil
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns a hash mapping file/dir names to route entries.
|
192
|
+
#
|
193
|
+
# @param dir [String] directory path to scan for files
|
194
|
+
# @param rel_path [String] directory path relative to root directory
|
195
|
+
# @param parent [Hash] directory's corresponding route entry
|
196
|
+
def compute_child_routes(dir:, rel_path:, parent:)
|
197
|
+
file_search(dir).each_with_object({}) { |fn, map|
|
198
|
+
next if File.basename(fn) =~ /^_/
|
199
|
+
|
200
|
+
rel_path = compute_clean_url_path(fn)
|
201
|
+
child = if File.file?(fn)
|
202
|
+
compute_route_file(fn:, rel_path:, parent:)
|
203
|
+
elsif File.directory?(fn)
|
204
|
+
compute_route_directory(dir: fn, rel_path:, parent:)
|
205
|
+
end
|
206
|
+
map[child_key(child)] = child if child
|
207
|
+
}
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns all entries in the given dir.
|
211
|
+
#
|
212
|
+
# @param dir [String] directory path
|
213
|
+
# @return [Array<String>] array of file entries
|
214
|
+
def file_search(dir)
|
215
|
+
Dir[File.join(dir.gsub(/[\[\]]/) { "\\#{it}"}, '*')]
|
216
|
+
end
|
217
|
+
|
218
|
+
# Computes a route entry and/or target for the given file path.
|
219
|
+
#
|
220
|
+
# @param fn [String] file path
|
221
|
+
# @param rel_path [String] relative path
|
222
|
+
# @param parent [Hash, nil] parent entry
|
223
|
+
# @return [void]
|
224
|
+
def compute_route_file(fn:, rel_path:, parent:)
|
225
|
+
abs_path = rel_path_to_abs_path(rel_path)
|
226
|
+
case
|
227
|
+
# index.rb, index+.rb, index.md
|
228
|
+
when (m = fn.match(/\/index(\+)?(\.(?:rb|md))$/))
|
229
|
+
# Index files (modules or markdown) files) are applied as targets to the
|
230
|
+
# immediate containing directory. A + suffix indicates this route
|
231
|
+
# handles requests to its subtree
|
232
|
+
plus, ext = m[1..2]
|
233
|
+
kind = FILE_TYPE[ext]
|
234
|
+
handle_subtree = (plus == '+') && (kind == :module)
|
235
|
+
set_index_route_target(parent:, path: abs_path, kind:, fn:, handle_subtree:)
|
236
|
+
|
237
|
+
# index.html
|
238
|
+
when fn.match(/\/index\.html$/)
|
239
|
+
# HTML index files are applied as targets to the immediate containing
|
240
|
+
# directory. It is considered static and therefore not added to the
|
241
|
+
# routing tree.
|
242
|
+
set_index_route_target(parent:, path: abs_path, kind: :static, fn:)
|
243
|
+
|
244
|
+
# foo.rb, foo+.rb, foo.md, [foo].rb, [foo]+.rb
|
245
|
+
when (m = fn.match(/\/(\[)?([^\]\/\+]+)(\])?(\+)?(\.(?:rb|md))$/))
|
246
|
+
# Module and markdown route targets. A + suffix indicates the module
|
247
|
+
# also handles requests to the subtree. For example, `/foo/bar.rb` will
|
248
|
+
# handle requests to `/foo/bar`, but `/foo/bar+.rb` will also handle
|
249
|
+
# requests to `/foo/bar/baz/bug`.
|
250
|
+
#
|
251
|
+
# parametric, or wildcard, routes convert segments of the URL path into
|
252
|
+
# parameters that are added to the HTTP request. Parametric routes are
|
253
|
+
# denoted using square brackets around the file/directory name. For
|
254
|
+
# example, `/api/posts/[id].rb`` will handle requests to
|
255
|
+
# `/api/posts/42`, and will extract the parameter `posts => 42` to add
|
256
|
+
# to the incoming request.
|
257
|
+
#
|
258
|
+
# A + suffix indicates the module also handles the subtree, so e.g.
|
259
|
+
# `/api/posts/[id]+.rb` will also handle requests to
|
260
|
+
# `/api/posts/42/fans` etc.
|
261
|
+
ob, param, cb, plus, ext = m[1..5]
|
262
|
+
kind = FILE_TYPE[ext]
|
263
|
+
make_route_entry(
|
264
|
+
parent:,
|
265
|
+
path: abs_path,
|
266
|
+
param: ob && cb ? param : nil,
|
267
|
+
target: make_route_target(kind:, fn:),
|
268
|
+
handle_subtree: (plus == '+') && (kind == :module)
|
269
|
+
)
|
270
|
+
|
271
|
+
# foo.html
|
272
|
+
when (m = fn.match(/\/[^\/]+\.html$/))
|
273
|
+
# HTML files are routed by their clean URL, i.e. without the `.html`
|
274
|
+
# extension. Those are considered as static routes, and do not have
|
275
|
+
# entries in the routing tree. Instead they are stored in the static
|
276
|
+
# map.
|
277
|
+
make_route_entry(
|
278
|
+
parent: parent,
|
279
|
+
path: abs_path,
|
280
|
+
target: make_route_target(kind: :static, fn:)
|
281
|
+
)
|
282
|
+
|
283
|
+
# everything else
|
284
|
+
else
|
285
|
+
# static files resolved using the static map, and are not added to the
|
286
|
+
# routing tree, which is used for resolving dynamic routes.
|
287
|
+
make_route_entry(
|
288
|
+
parent: parent,
|
289
|
+
path: abs_path,
|
290
|
+
target: { kind: :static, fn: fn }
|
291
|
+
)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Sets an index route target for the given parent entry.
|
296
|
+
#
|
297
|
+
# @param parent [Hash] parent route entry
|
298
|
+
# @param path [String] route path
|
299
|
+
# @param kind [Symbol] route target kind
|
300
|
+
# @param fn [String] route target filename
|
301
|
+
# @param handle_subtree [bool] whether the target handles the subtree
|
302
|
+
# @return [nil] (prevents addition of an index route)
|
303
|
+
def set_index_route_target(parent:, path:, kind:, fn:, handle_subtree: nil)
|
304
|
+
if is_parametric_route?(parent) || handle_subtree
|
305
|
+
@dynamic_map[path] = parent
|
306
|
+
parent[:target] = make_route_target(kind:, fn:)
|
307
|
+
parent[:handle_subtree] = handle_subtree
|
308
|
+
else
|
309
|
+
@static_map[path] = {
|
310
|
+
parent: parent[:parent],
|
311
|
+
path: path,
|
312
|
+
target: { kind:, fn: },
|
313
|
+
# In case we're at the tree root, we need to copy over the hook and
|
314
|
+
# error refs.
|
315
|
+
hook: !parent[:parent] && parent[:hook],
|
316
|
+
error: !parent[:parent] && parent[:error]
|
317
|
+
}
|
318
|
+
end
|
319
|
+
nil
|
320
|
+
end
|
321
|
+
|
322
|
+
# Creates a new route entry, registering it in the static or dynamic map,
|
323
|
+
# according to its type.
|
324
|
+
#
|
325
|
+
# @param entry [Hash] route entry
|
326
|
+
def make_route_entry(entry)
|
327
|
+
path = entry[:path]
|
328
|
+
if is_parametric_route?(entry) || entry[:handle_subtree]
|
329
|
+
@dynamic_map[path] = entry
|
330
|
+
else
|
331
|
+
entry[:static] = true
|
332
|
+
@static_map[path] = entry
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# returns true if the route or any of its ancestors are parametric.
|
337
|
+
#
|
338
|
+
# @param entry [Hash] route entry
|
339
|
+
def is_parametric_route?(entry)
|
340
|
+
entry[:param] || (entry[:parent] && is_parametric_route?(entry[:parent]))
|
341
|
+
end
|
342
|
+
|
343
|
+
# Converts a relative URL path to absolute URL path.
|
344
|
+
#
|
345
|
+
# @param rel_path [String] relative path
|
346
|
+
# @return [String] absolute path
|
347
|
+
def rel_path_to_abs_path(rel_path)
|
348
|
+
rel_path == '/' ? @mount_path : File.join(@mount_path, rel_path)
|
349
|
+
end
|
350
|
+
|
351
|
+
# Returns the key for the given route entry to be used in its parent's
|
352
|
+
# children map.
|
353
|
+
#
|
354
|
+
# @param entry [Hash] route entry
|
355
|
+
# @return [String] child key
|
356
|
+
def child_key(entry)
|
357
|
+
entry[:param] ? '[]' : File.basename(entry[:path]).gsub(/\+$/, '')
|
358
|
+
end
|
359
|
+
|
360
|
+
# Returns a hash representing a route target for the given route target kind
|
361
|
+
# and file name.
|
362
|
+
#
|
363
|
+
# @param kind [Symbol] route target kind
|
364
|
+
# @param fn [String] filename
|
365
|
+
# @return [Hash] route target hash
|
366
|
+
def make_route_target(kind:, fn:)
|
367
|
+
{
|
368
|
+
kind: kind,
|
369
|
+
fn: fn
|
370
|
+
}
|
371
|
+
end
|
372
|
+
|
373
|
+
# Generates and returns a router proc based on the routing tree.
|
374
|
+
#
|
375
|
+
# @return [Proc] router proc
|
376
|
+
def compile_router_proc
|
377
|
+
code = generate_routing_tree_code
|
378
|
+
eval(code, binding, '(router)', 1)
|
379
|
+
end
|
380
|
+
|
381
|
+
# Generates the router proc source code. The router proc code is dynamically
|
382
|
+
# generated from the routing tree, converting the routing tree structure into
|
383
|
+
# Ruby proc of the following signature:
|
384
|
+
#
|
385
|
+
# ```ruby
|
386
|
+
# # @param path [String] URL path
|
387
|
+
# # @param params [Hash] Hash receiving parametric route values
|
388
|
+
# # @return [Hash, nil] route entry
|
389
|
+
# ->(path, params) { ... }
|
390
|
+
# ```
|
391
|
+
#
|
392
|
+
# The generated code performs the following tasks:
|
393
|
+
#
|
394
|
+
# - Test if the given path corresponds to a static file (using `@static_map`)
|
395
|
+
# - Otherwise, split the given path into path segments
|
396
|
+
# - Walk through the path segments according to the routing tree structure
|
397
|
+
# - Emit parametric route values to the `params` hash
|
398
|
+
# - Return the found route entry
|
399
|
+
#
|
400
|
+
# @return [String] router proc code to be `eval`ed
|
401
|
+
def generate_routing_tree_code
|
402
|
+
buffer = +''
|
403
|
+
buffer << "# frozen_string_literal: true\n"
|
404
|
+
|
405
|
+
emit_code_line(buffer, '->(path, params) {')
|
406
|
+
emit_code_line(buffer, ' entry = @static_map[path]; return entry if entry')
|
407
|
+
emit_code_line(buffer, ' parts = path.split("/")')
|
408
|
+
|
409
|
+
if @root[:path] != '/'
|
410
|
+
root_parts = @root[:path].split('/')
|
411
|
+
segment_idx = root_parts.size
|
412
|
+
validate_parts = []
|
413
|
+
(1..(segment_idx - 1)).each do |i|
|
414
|
+
validate_parts << "(parts[#{i}] != #{root_parts[i].inspect})"
|
415
|
+
end
|
416
|
+
emit_code_line(buffer, " return nil if #{validate_parts.join(' || ')}")
|
417
|
+
else
|
418
|
+
segment_idx = 1
|
419
|
+
end
|
420
|
+
|
421
|
+
visit_routing_tree_entry(buffer:, entry: @root, segment_idx:)
|
422
|
+
|
423
|
+
emit_code_line(buffer, " return nil")
|
424
|
+
emit_code_line(buffer, "}")
|
425
|
+
buffer#.tap { puts '*' * 40; puts it; puts }
|
426
|
+
end
|
427
|
+
|
428
|
+
# Generates routing logic code for the given route entry.
|
429
|
+
#
|
430
|
+
# @param buffer [String] buffer receiving code
|
431
|
+
# @param entry [Hash] route entry
|
432
|
+
# @param indent [Integer] indent level
|
433
|
+
# @param segment_idx [Integer] path segment index
|
434
|
+
# @return [void]
|
435
|
+
def visit_routing_tree_entry(buffer:, entry:, indent: 1, segment_idx:)
|
436
|
+
ws = ' ' * (indent * 2)
|
437
|
+
|
438
|
+
# If no targets exist in the entry's subtree, we can return nil
|
439
|
+
# immediately.
|
440
|
+
if !entry[:target] && !find_target_in_subtree(entry)
|
441
|
+
emit_code_line(buffer, "#{ws}return nil")
|
442
|
+
return
|
443
|
+
end
|
444
|
+
|
445
|
+
if is_void_route?(entry)
|
446
|
+
parent = entry[:parent]
|
447
|
+
parametric_sibling = parent && parent[:children] && parent[:children]['[]']
|
448
|
+
if parametric_sibling
|
449
|
+
emit_code_line(buffer, "#{ws}return nil")
|
450
|
+
return
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
# Get next segment
|
455
|
+
emit_code_line(buffer, "#{ws}case (p = parts[#{segment_idx}])")
|
456
|
+
|
457
|
+
# In case of no next segment
|
458
|
+
emit_code_line(buffer, "#{ws}when nil")
|
459
|
+
if entry[:target]
|
460
|
+
map = entry[:static] ? '@static_map' : '@dynamic_map'
|
461
|
+
emit_code_line(buffer, "#{ws} return #{map}[#{entry[:path].inspect}]")
|
462
|
+
else
|
463
|
+
emit_code_line(buffer, "#{ws} return nil")
|
464
|
+
end
|
465
|
+
|
466
|
+
if entry[:children]
|
467
|
+
param_entry = entry[:children]['[]']
|
468
|
+
entry[:children].each do |k, child_entry|
|
469
|
+
# skip if wildcard entry (treated in else clause below)
|
470
|
+
next if k == '[]'
|
471
|
+
|
472
|
+
# skip if entry is void (no target, no children)
|
473
|
+
has_target = child_entry[:target]
|
474
|
+
has_children = child_entry[:children] && !child_entry[:children].empty?
|
475
|
+
next if !has_target && !has_children
|
476
|
+
|
477
|
+
if has_target && !has_children
|
478
|
+
# use the target
|
479
|
+
next if child_entry[:static]
|
480
|
+
|
481
|
+
emit_code_line(buffer, "#{ws}when #{k.inspect}")
|
482
|
+
if_clause = child_entry[:handle_subtree] ? '' : " if !parts[#{segment_idx + 1}]"
|
483
|
+
route_value = "@dynamic_map[#{child_entry[:path].inspect}]"
|
484
|
+
emit_code_line(buffer, "#{ws} return #{route_value}#{if_clause}")
|
485
|
+
|
486
|
+
elsif has_children
|
487
|
+
# otherwise look at the next segment
|
488
|
+
next if is_void_route?(child_entry) && !param_entry
|
489
|
+
|
490
|
+
emit_code_line(buffer, "#{ws}when #{k.inspect}")
|
491
|
+
visit_routing_tree_entry(buffer:, entry: child_entry, indent: indent + 1, segment_idx: segment_idx + 1)
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# parametric route
|
496
|
+
if param_entry
|
497
|
+
emit_code_line(buffer, "#{ws}else")
|
498
|
+
emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = p")
|
499
|
+
visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
|
500
|
+
end
|
501
|
+
end
|
502
|
+
emit_code_line(buffer, "#{ws}end")
|
503
|
+
end
|
504
|
+
|
505
|
+
# Returns the first target found in the given entry's subtree.
|
506
|
+
#
|
507
|
+
# @param entry [Hash] route entry
|
508
|
+
# @return [Hash, nil] route target if exists
|
509
|
+
def find_target_in_subtree(entry)
|
510
|
+
entry[:children]&.values&.each { |e|
|
511
|
+
target = e[:target] || find_target_in_subtree(e)
|
512
|
+
return target if target
|
513
|
+
}
|
514
|
+
|
515
|
+
nil
|
516
|
+
end
|
517
|
+
|
518
|
+
# Returns true if the given route is not parametric, has no children and is
|
519
|
+
# static, or has children and all are void.
|
520
|
+
#
|
521
|
+
# @param entry [Hash] route entry
|
522
|
+
# @return [bool]
|
523
|
+
def is_void_route?(entry)
|
524
|
+
return false if entry[:param]
|
525
|
+
|
526
|
+
if entry[:children]
|
527
|
+
return true if !entry[:children]['[]'] && entry[:children]&.values&.all? { is_void_route?(it) }
|
528
|
+
else
|
529
|
+
return true if entry[:static]
|
530
|
+
end
|
531
|
+
|
532
|
+
false
|
533
|
+
end
|
534
|
+
|
535
|
+
DEBUG = !!ENV['DEBUG']
|
536
|
+
|
537
|
+
# Emits the given code into the given buffer, with a line break at the end.
|
538
|
+
# If the `DEBUG` environment variable is set, adds a source location comment
|
539
|
+
# at the end of the line, referencing the callsite.
|
540
|
+
#
|
541
|
+
# @param buffer [String] code buffer
|
542
|
+
# @param code [String] code
|
543
|
+
# @return [void]
|
544
|
+
def emit_code_line(buffer, code)
|
545
|
+
if DEBUG
|
546
|
+
loc = (m = caller[0].match(/^([^\:]+\:\d+)/)) && m[1]
|
547
|
+
buffer << "#{code} # #{loc}\n"
|
548
|
+
else
|
549
|
+
buffer << "#{code}\n"
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
data/syntropy.gemspec
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
s.add_dependency 'json', '2.13.2'
|
26
26
|
s.add_dependency 'p2', '2.8'
|
27
27
|
s.add_dependency 'papercraft', '1.4'
|
28
|
-
s.add_dependency 'qeweney', '0.
|
28
|
+
s.add_dependency 'qeweney', '0.22'
|
29
29
|
s.add_dependency 'tp2', '0.14.1'
|
30
30
|
s.add_dependency 'uringmachine', '0.16'
|
31
31
|
|
data/test/helper.rb
CHANGED
@@ -6,13 +6,14 @@ require 'uringmachine'
|
|
6
6
|
require 'syntropy'
|
7
7
|
require 'qeweney/mock_adapter'
|
8
8
|
require 'minitest/autorun'
|
9
|
+
require 'fileutils'
|
9
10
|
|
10
11
|
STDOUT.sync = true
|
11
12
|
STDERR.sync = true
|
12
13
|
|
13
14
|
module ::Kernel
|
14
|
-
def mock_req(
|
15
|
-
Qeweney::MockAdapter.mock(
|
15
|
+
def mock_req(headers, body = nil)
|
16
|
+
Qeweney::MockAdapter.mock(headers, body).tap { it.setup_mock_request }
|
16
17
|
end
|
17
18
|
|
18
19
|
def capture_exception
|
@@ -48,6 +49,21 @@ module ::Kernel
|
|
48
49
|
def monotonic_clock
|
49
50
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
50
51
|
end
|
52
|
+
|
53
|
+
def make_tmp_file_tree(dir, spec)
|
54
|
+
FileUtils.mkdir(dir) rescue nil
|
55
|
+
spec.each do |k, v|
|
56
|
+
fn = File.join(dir, k.to_s)
|
57
|
+
case v
|
58
|
+
when String
|
59
|
+
IO.write(fn, v)
|
60
|
+
when Hash
|
61
|
+
FileUtils.mkdir(fn) rescue nil
|
62
|
+
make_tmp_file_tree(fn, v)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
dir
|
66
|
+
end
|
51
67
|
end
|
52
68
|
|
53
69
|
module Minitest::Assertions
|